{
  "openapi": "3.1.0",
  "info": {
    "title": "ViewStream API",
    "description": "The ViewStream API lets your CMS, newsroom tools and backends do everything ViewStream Studio does: register and\npublish video, run live channels and their programme guide, cut clips, add subtitles, read audience statistics,\nprotect playback and receive events.\n\n> **Try it out calls the production API.** Requests you send from this page are real: they create, change and\n> delete data of the tenant your API key belongs to. Use a key with read-only scopes while exploring. Your key is\n> kept only in this browser tab's memory — it is never stored or sent anywhere except in the `Authorization`\n> header of the requests you make.\n\n## Base URL\n\n`https://api.viewstream.co.il` — every path below starts with `/v1`. The API is JSON over HTTPS (TLS 1.2+).\nTimestamps are RFC 3339 in UTC (`2026-09-30T08:15:00Z`); ids are UUIDs.\n\n## Authentication\n\nSend an API key as a bearer token:\n\n```\nAuthorization: Bearer vs_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nCreate keys in **Studio → Integrations → API keys** (or with `POST /v1/api-keys`). The full key is shown **once**;\nViewStream stores only a hash. A key belongs to one tenant and carries a fixed set of scopes. Revoke a key at any\ntime; revoked keys answer `401`. Keep keys on your servers — never ship one in a web page or an app.\n\n## Scopes\n\nEvery operation lists the scope it needs as **Required scope** (and `x-required-scope` in the spec). A key\nwithout it gets `403 insufficient_scope`. A few operations check an extra scope only for one field (for example\n`publish` on an asset needs `assets:publish`); their descriptions say so. Studio users get scopes from their\nrole; the last column shows the lowest role that has each scope.\n\n| Scope | Allows | Lowest Studio role that has it |\n|---|---|---|\n| `assets:read` | List and read assets, jobs, trash, library sections, posters, subtitle tracks and library imports; the tenant branding and defaults. | viewer |\n| `assets:write` | Register, edit, re-encode and delete assets; uploads of posters; library sections; library imports; retry jobs; edit subtitle cues. | editor |\n| `assets:publish` | Publish an asset (`publish` on create or patch). | publisher |\n| `assets:purge` | Delete trashed assets permanently, empty the trash, set its retention. | admin |\n| `clips:read` | List and read clips. | viewer |\n| `clips:write` | Cut and delete clips. | editor |\n| `clips:publish` | Publish a clip (`publish: true`). | publisher |\n| `channels:read` | Read channels, programmes (EPG), recordings, catch-up lists, boundaries and blackouts. | viewer |\n| `channels:write` | Create and change channels, their EPG sources and programmes; also edits and publishes the guide. | engineer |\n| `channels:operate` | Start/stop channels, see ingest secrets, rotate the SRT passphrase. | engineer |\n| `uploads` | Browser/API multipart uploads (`/v1/uploads`). | editor |\n| `prewarm` | Pre-warm the edges. | engineer |\n| `delivery:write` | Purge, CDN distributions, playback policies and attach points, player configurations, blackouts. | engineer |\n| `webhooks:manage` | Outbound webhooks, inbound CMS hooks and event-export destinations. | engineer |\n| `storage:manage` | Storage keys of the tenant. | engineer |\n| `keys:manage` | API keys and playback signing keys. | engineer |\n| `stats:read` | Statistics, traffic, sessions (without viewer ids), distributions and policies (read), leaks and revocations (read). | viewer |\n| `stats:pii` | Viewer ids in session search and hashed IPs in event export. | admin |\n| `events:read` | The server-sent event stream and the Studio inbox. | viewer |\n| `team:manage` | Members, roles and invitations. | admin |\n| `notifications:manage` | Notification rules, monitors and alert destinations (e-mail, Telegram); acknowledging alerts. | admin |\n| `billing:read` | Usage figures of the tenant (`GET /v1/billing/usage`). | owner |\n| `tenant:settings` | Tenant-wide settings — branding, defaults, subtitles, packaging, image upscaling, lip-sync, catch-up exclusions. | owner |\n| `sites:read` | Read ViewStream Sites objects (sites, pages, shows, people, menus, theme). | viewer |\n| `sites:write` | Edit sites, pages, shows and people (drafts). | editor |\n| `sites:publish` | Publish pages, menus and themes. | publisher |\n| `sites:admin` | Create/delete sites, domains, routes, theme CSS. | engineer |\n| `playback:sign` | Issue tokenised playback URLs for your backend. | engineer |\n| `security:manage` | Dismiss leaks, revoke playback sessions, leak settings, A/B watermark settings, variants and traces of leaked recordings. | engineer |\n| `epg:write` | Edit the programme guide (staged as a draft in review mode). | editor |\n| `epg:publish` | Publish the guide and manage its publishing destinations. | publisher |\n| `support:read` | Read the tenant's support requests and their conversation. | viewer |\n| `support:write` | Open support requests, reply to them and close them. | viewer |\n| `ai:read` | Read AI artefacts (article drafts and published articles) and the review queue. | viewer |\n| `ai:write` | Edit AI artefacts before they are published, and reject them. | editor |\n| `ai:publish` | Approve and publish AI artefacts (the site shows them; artifact.published fires). | publisher |\n\n## Errors\n\nErrors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details with\n`Content-Type: application/problem+json`:\n\n```json\n{\n  \"type\": \"https://viewstream.co.il/problems/insufficient_scope\",\n  \"title\": \"The API key lacks the scope this route needs\",\n  \"status\": 403,\n  \"detail\": \"this route requires scope assets:write\",\n  \"request_id\": \"019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d\"\n}\n```\n\n| `type` suffix | Status | Meaning |\n|---|---|---|\n| `invalid_credentials` | 401 | Missing, malformed, unknown or revoked API key |\n| `insufficient_scope` | 403 | The key lacks the scope in `detail` |\n| `forbidden` | 403 | Not allowed for this tenant or object |\n| `feature_disabled` | 403 / 503 | Not available to this tenant (e.g. CDN-only tenants have no assets, channels, clips or players) or not enabled on this deployment |\n| `not_found` | 404 | No such object in your tenant |\n| `conflict` | 409 | The change conflicts with the current state (duplicate `external_id`, stale `If-Match`, …) |\n| `validation_error` | 422 | The body or query is invalid; `errors[]` lists `{field, detail}` |\n| `lock_window` | 409 | The guide change reaches into the channel's locked hours |\n| `rate_limited` | 429 | Too many requests; wait `Retry-After` seconds |\n| `internal_error` | 500 | Our fault — quote the `request_id` to support |\n| `not_ready` | 503 | A dependency is unavailable; retry with backoff |\n\nEvery response carries `X-Request-Id`; include it when you contact support.\n\n## Pagination\n\nLong lists are cursor-paginated, newest first: pass `limit` (the maximum is documented per operation) and read\n`next_cursor` from the response. Send it back as `?cursor=` for the next page; `next_cursor: null` means the end.\nCursors are opaque — do not build or parse them. Lists without `cursor` return everything (or the newest N, as\ntheir description says).\n\n```\nGET /v1/assets?limit=50\n→ {\"items\": [...50 assets...], \"next_cursor\": \"MDE5Mjg2YTI...\"}\nGET /v1/assets?limit=50&cursor=MDE5Mjg2YTI...\n→ {\"items\": [...], \"next_cursor\": null}\n```\n\n## Rate limits\n\nEach API key may send **20 requests per second** with bursts of up to **100**. Over the limit the API answers\n`429 rate_limited` with `Retry-After` (seconds). Some operations have their own limits, stated in their\ndescriptions (edge pre-warm: 6 per minute per tenant; the playback checker: 12 runs per minute per tenant).\nBack off exponentially on `429` and `503`.\n\n## Idempotency and caching\n\n`GET`, `PUT` and `DELETE` are idempotent. `POST /v1/assets` with an `external_id` is safe to retry: a duplicate\nanswers `409` instead of creating a second asset. API responses are `Cache-Control: no-store`.\n\n## Events\n\nSubscribe to webhooks (`/v1/webhooks`) for `asset.ready`, `clip.ready` and the other events. Deliveries are signed:\n`X-VS-Signature: sha256=<hex HMAC-SHA256(secret, X-VS-Timestamp + \".\" + raw body)>`; reject timestamps older\nthan 5 minutes. For analytics pipelines use event export (`/v1/event-destinations`) to S3, Kafka, HTTPS or GA4.\n\n## Sites Delivery API\n\nThe public, cacheable read API behind ViewStream Sites (`/s/v1`) has its own document — pick\n**Sites Delivery API** at the top of this page.\n",
    "contact": {
      "name": "ViewStream by Interhost Networks",
      "url": "https://www.viewstream.co.il"
    },
    "license": {
      "name": "Proprietary — for ViewStream customers",
      "url": "https://www.viewstream.co.il"
    },
    "version": "0.2.0"
  },
  "servers": [
    {
      "url": "https://api.viewstream.co.il",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "assets",
      "description": "Video on demand. Register a source (an S3 key under your ingest prefix, a URL, or an upload), and ViewStream\nprobes, transcodes and packages it; the asset moves `registered → probing → queued → encoding → packaging →\nready`. Publish it to make it playable, patch its metadata, re-encode, or delete it (it goes to the trash and\ncan be restored until it is purged). Posters, people and subtitle tracks hang off assets.\n\nTypical flow: `POST /v1/assets` → wait for the `asset.ready` webhook (or poll `GET /v1/assets/{id}`) →\n`PATCH /v1/assets/{id}` with `{\"publish\": true}` (needs `assets:publish`).\n"
    },
    {
      "name": "uploads",
      "description": "Multipart uploads straight to ViewStream storage (parts of up to 64 MiB); complete the upload and register the asset."
    },
    {
      "name": "library",
      "description": "Library sections (collections) that group assets, one level of nesting; items can be added, removed and moved in bulk."
    },
    {
      "name": "library-imports",
      "description": "Import an existing video library from your website's sitemap: create an import, review the discovered items,\nstart it; ViewStream creates the assets and fetches the media in the background (idempotent — already imported\nitems are skipped).\n"
    },
    {
      "name": "jobs",
      "description": "The processing jobs of your tenant (probe, transcode, package, subtitles, …), their events and failures; retry failed jobs one by one or by group."
    },
    {
      "name": "channels",
      "description": "Live channels: create a channel, feed it (SRT or RTMP), start and stop it. Each channel is recorded continuously,\nwhich powers start-over, catch-up by programme, live-to-VOD publishing and clipping. Programme boundaries\n(air delay, openers) are detected automatically and can be corrected.\n"
    },
    {
      "name": "live",
      "description": "Ingest secrets of a live channel (SRT passphrase)."
    },
    {
      "name": "clips",
      "description": "Cut clips from a channel's recording by time range — playable immediately at segment precision, frame-accurate after finalising."
    },
    {
      "name": "epg",
      "description": "The electronic programme guide. Import from XMLTV or JSON, edit programmes, and (in `review` mode) stage edits\nin a draft that a publisher publishes as a version; roll back, copy days, shift overruns, recurring templates.\nPublish the guide to partners (pull feeds, HTTPS/SFTP/S3 pushes, DVB EIT). The public XMLTV/JSON exports are\nunder `/epg/…` and need no key.\n"
    },
    {
      "name": "subtitles",
      "description": "Automatic Hebrew subtitles (speech recognition) for catch-up and VOD, translations into other languages, a\nglossary, and a cue editor that saves new versions. Subtitles are added to the HLS playlists automatically.\n"
    },
    {
      "name": "posters",
      "description": "Poster images of assets, series and programmes — automatic stills or your own upload."
    },
    {
      "name": "images",
      "description": "AI image upscaling of small artwork (settings and per-image upscale/revert)."
    },
    {
      "name": "packaging",
      "description": "Output packaging per tenant and channel (fMP4, MPEG-TS or both)."
    },
    {
      "name": "lipsync",
      "description": "Lip-sync (audio/video offset) monitoring settings and results per channel."
    },
    {
      "name": "input-monitor",
      "description": "Continuous monitoring of a channel's input legs (primary a, backup b): availability, bitrate, format, TS and HLS\ncontinuity, black / frozen / silent pictures and sound, loudness (EBU R128) and A/V drift — off by default.\n"
    },
    {
      "name": "fast",
      "description": "FAST channels: linear channels assembled from your library and approved AI clips, without an encoder. Weekly\nschedules from rules (dayparts, rotation, repeat limits, fillers, break pattern), a playlist checker that blocks\npublishing on errors and alerts you, a \"best of\" channel programmed from approved clips, XMLTV/JSON guide\nentries, SSAI at the break markers and live break-ins — off by default.\n"
    },
    {
      "name": "settings",
      "description": "Tenant settings: branding (display name, logos for light and dark backgrounds, brand colours checked for WCAG AA\ncontrast, favicon, e-mail sender name) and the tenant-wide defaults in one view, with how many channels and\nvideos choose differently. Branding is the starting point of new player configurations and new Sites themes.\n"
    },
    {
      "name": "billing",
      "description": "Read-only monthly usage: storage, delivered traffic, transcoding and GPU minutes, live channel-days, Sites page\nviews and exported events, each with the source it is read from; also as CSV. Invoices are not available yet.\n"
    },
    {
      "name": "players",
      "description": "Player configurations (presets) and where they apply — tenant default, section, channel or asset."
    },
    {
      "name": "delivery",
      "description": "CDN operations — pre-warm and purge the edges, your own CDN distributions (hostnames, token auth, signing), their DNS/TLS status and the playback checker."
    },
    {
      "name": "protection",
      "description": "Playback protection: playback policies (referrer and embed-domain lists, geo, ASN and datacenter blocking,\nplayback tokens) attached to the tenant, channels, assets or distributions; signed playback URLs for your backend\n(`POST /v1/playback/tokens`, scope `playback:sign`); signing-key rotation; geo blackouts; leak detection and\nsession revocation.\n"
    },
    {
      "name": "insight",
      "description": "Audience and delivery statistics: realtime viewers, period overviews with comparison, timeseries, breakdowns,\ntop content, edge traffic and 95th percentile, returning viewers, programme ratings, ads, protection denials and\nsession search. Time ranges take `from`/`to` as RFC 3339 or epoch ms.\n"
    },
    {
      "name": "events",
      "description": "A server-sent event stream (`text/event-stream`) of your tenant's events — the same events webhooks carry, plus realtime statistics."
    },
    {
      "name": "webhooks",
      "description": "Outbound webhooks: register HTTPS endpoints for event types; deliveries are signed (see *Events* above) and\nretried for up to ~15 hours. Inspect deliveries and redeliver. Inbound hooks let your CMS call ViewStream with\na signed request and no API key.\n"
    },
    {
      "name": "event-export",
      "description": "Stream statistics and platform events to your own systems — S3 (gzip NDJSON), Kafka, HTTPS (HMAC-signed\nbatches) or Google Analytics 4 — with a delivery log, a dead-letter queue and replay. A built-in test receiver\nlets you try it without an endpoint.\n"
    },
    {
      "name": "sites",
      "description": "ViewStream Sites management: shows (series), people, sites, pages with drafts, versions and scheduling,\ntheme, menus, routes, redirects and custom domains. The rendered site reads the Sites Delivery API.\n"
    },
    {
      "name": "storage",
      "description": "Storage usage of the tenant and its S3-compatible keys."
    },
    {
      "name": "keys",
      "description": "API keys of the tenant (scope `keys:manage`). The secret is returned once, on creation."
    },
    {
      "name": "team",
      "description": "Members of the tenant, their roles, and invitations."
    },
    {
      "name": "invitations",
      "description": "Invite people to the tenant."
    },
    {
      "name": "notifications",
      "description": "Notification rules (email, Telegram) and the in-app inbox."
    },
    {
      "name": "monitoring",
      "description": "Monitors and alerts for your own channels, videos and sites: live stream checks (feed down / slate, no new\nsegments, encoder below real time, recorder gaps, live playlist errors at the edge, black video, lip-sync\ndrift), quality of experience (startup p95, rebuffering, playback errors, plays dropping during a programme)\nand delivery (subtitles failures, waiting translations). Each check has a threshold, a duration and a\nseverity; monitors have quiet hours and snooze. Alerts go to the Studio bell, to e-mail (double opt-in for\naddresses outside your team) and Telegram (your own chat, linked through the ViewStream bot), and to your\nwebhooks as `alert.firing` / `alert.resolved`.\n"
    },
    {
      "name": "support",
      "description": "Contact ViewStream support (Interhost): open a request (question, bug, outage, billing, feature request),\nfollow the conversation, reply and close it. Scopes `support:read` / `support:write`; at most 10 new requests\nper tenant per hour.\n"
    },
    {
      "name": "mcp",
      "description": "The remote MCP server at `https://api.viewstream.co.il/mcp`: AI assistants (Claude, ChatGPT, Cursor, …) use\nViewStream with an API key — search catch-up and the library, read EPG, subtitles and summaries, statistics,\nalerts and the manual; cut clips and open support requests when the key allows it. Model Context Protocol over\nStreamable HTTP; see the API guide, \"MCP server\".\n"
    },
    {
      "name": "auth",
      "description": "Who the key belongs to."
    },
    {
      "name": "tenants",
      "description": "The tenant the key belongs to."
    },
    {
      "name": "health",
      "description": "Liveness of the API."
    },
    {
      "name": "meta",
      "description": "This documentation and the machine-readable OpenAPI documents."
    },
    {
      "name": "ai",
      "description": "AI newsroom — article drafts of programmes and the editorial review queue (every AI artefact is a draft until an editor publishes it)"
    }
  ],
  "paths": {
    "/docs": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "Interactive API documentation (Swagger UI); redirects to /docs/",
        "description": "Answers `301` with `Location: /docs/`. The page and its assets are served under `/docs/` (self-hosted Swagger UI\nfrom this origin only, strict Content-Security-Policy, `X-Frame-Options: DENY`, cached 5 minutes; the vendored\nfiles under `/docs/vendor/` are immutable for a year). No authentication. Try it out uses the key typed into\nAuthorize and calls this API from the browser; nothing is stored server-side. Unknown files under `/docs/` answer 404.",
        "operationId": "getDocs",
        "responses": {
          "301": {
            "description": "Redirect to /docs/",
            "headers": {
              "Location": {
                "description": "Always `/docs/`",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi.yaml": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "This API as an OpenAPI 3.1 document (YAML)",
        "description": "The public document of the ViewStream API, generated at build time from the source specification: internal,\noperator, edge-only and Studio-session-only routes are removed, the only security scheme is the API key and every\noperation names its required scope (`x-required-scope`). No authentication; `Access-Control-Allow-Origin: *` so\ncode generators and API tools can fetch it cross-origin; `Cache-Control: public, max-age=300`. The Sites Delivery\nAPI (`/s/v1`) has its own document at `/openapi/sites-delivery.yaml`.",
        "operationId": "getOpenAPIYAML",
        "responses": {
          "200": {
            "description": "The document (`application/yaml; charset=utf-8`)",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "ViewStream API",
                    "version": "0.2.0"
                  },
                  "paths": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/mcp": {
      "post": {
        "tags": [
          "mcp"
        ],
        "summary": "Remote MCP server (Model Context Protocol, Streamable HTTP) for AI assistants — Claude, ChatGPT, Cursor",
        "description": "**Required scope:** none — any valid API key of the tenant.\n\nOne JSON-RPC 2.0 message per POST (MCP revision 2025-06-18; 2025-11-25 and 2025-03-26 are negotiated in\n`initialize`, and 2025-03-26 batches of at most 20 messages are accepted). A request gets one\n`application/json` response; notifications (`notifications/initialized`, …) get `202` with no body. Methods:\n`initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`,\n`resources/templates/list`, `prompts/list`, `prompts/get`. Stateless: no `Mcp-Session-Id` is issued.\n`MCP-Protocol-Version` is checked when sent (400 for an unsupported revision).\n\nAuthentication is a tenant API key (`Authorization: Bearer <key>`, same rules and rate limit as `/v1`; a\nStudio session cookie is not accepted). `tools/list` shows only the tools the key's scopes allow; each tool\nnames its scope (read tools: `channels:read`, `assets:read`, `stats:read`; write tools: `clips:write`,\n`support:write`, `assets:write`). Tools call the `/v1` API in-process as the key, so tenant isolation,\nvalidation and audit are the API's own; every `tools/call` is also audited as `mcp.tool_call`. A failed\ntool answers a result with `isError: true` and the API's problem detail; protocol errors are JSON-RPC\nerrors. Tool results are capped at about 60 KB (lists are shortened and marked `_truncated`).\n\nResources: `viewstream://docs/{lang}/{topic}` (the Studio manual and the API guide, en | he),\n`viewstream://openapi` (every operation with its scope) and `viewstream://openapi/{operationId}`.\nBrowser requests whose `Origin` is not on the allow-list are refused (403, DNS-rebinding protection); MCP\nclients send no `Origin`. Guide: docs/guide/api/mcp.md.",
        "operationId": "postMCP",
        "parameters": [
          {
            "name": "MCP-Protocol-Version",
            "in": "header",
            "description": "The negotiated revision (sent by clients after initialize)",
            "schema": {
              "type": "string",
              "enum": [
                "2025-11-25",
                "2025-06-18",
                "2025-03-26"
              ]
            }
          },
          {
            "name": "Accept",
            "in": "header",
            "description": "application/json, text/event-stream (the response is always application/json)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "A JSON-RPC 2.0 request or notification (or, for 2025-03-26 clients, an array of them)",
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "properties": {
                  "jsonrpc": {
                    "type": "string",
                    "enum": [
                      "2.0"
                    ]
                  },
                  "id": {
                    "type": [
                      "string",
                      "integer"
                    ]
                  },
                  "method": {
                    "type": "string"
                  },
                  "params": {
                    "type": "object"
                  }
                }
              },
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {
                  "name": "search_catchup",
                  "arguments": {
                    "q": "אליפות אירופה",
                    "limit": 5
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The JSON-RPC response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "type": "string"
                    },
                    "id": {
                      "type": [
                        "string",
                        "integer",
                        "null"
                      ]
                    },
                    "result": {
                      "type": "object"
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        },
                        "data": {}
                      }
                    }
                  }
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "result": {
                    "content": [
                      {
                        "type": "text",
                        "text": "{\"q\":\"אליפות אירופה\",\"items\":[…]}"
                      }
                    ],
                    "structuredContent": {
                      "q": "אליפות אירופה",
                      "items": [],
                      "truncated": false,
                      "channels_searched": 1
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Notification or response accepted (no body)"
          },
          "400": {
            "description": "Unsupported MCP-Protocol-Version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "No, malformed, unknown or revoked API key (`WWW-Authenticate: Bearer`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "description": "Browser Origin not on the allow-list, or a key without a tenant",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "406": {
            "description": "Accept excludes application/json",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "413": {
            "description": "Body over 1 MiB",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type is not application/json",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "mcp"
        ],
        "summary": "Server-initiated SSE stream — not offered (405)",
        "description": "The MCP Streamable HTTP transport lets a client open an SSE stream with GET; this server sends no\nserver-initiated messages and answers `405 Method Not Allowed` (`Allow: POST`). Not authenticated.",
        "operationId": "getMCP",
        "responses": {
          "405": {
            "description": "Always",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "This API as an OpenAPI 3.1 document (JSON)",
        "description": "The same public document as `/openapi.yaml`, as JSON (`application/json; charset=utf-8`). No authentication;\n`Access-Control-Allow-Origin: *`; `Cache-Control: public, max-age=300`.",
        "operationId": "getOpenAPIJSON",
        "responses": {
          "200": {
            "description": "The document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "ViewStream API",
                    "version": "0.2.0"
                  },
                  "paths": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi/sites-delivery.yaml": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "The Sites Delivery API as an OpenAPI 3.1 document (YAML)",
        "description": "The public document of the Sites Delivery API (`/s/v1/…`, the cacheable read API behind ViewStream Sites),\ngenerated from the same source as `/openapi.yaml`. No authentication; `Access-Control-Allow-Origin: *`;\n`Cache-Control: public, max-age=300`.",
        "operationId": "getSitesDeliveryOpenAPIYAML",
        "responses": {
          "200": {
            "description": "The document (`application/yaml; charset=utf-8`)",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "ViewStream Sites Delivery API"
                  },
                  "paths": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi/sites-delivery.json": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "The Sites Delivery API as an OpenAPI 3.1 document (JSON)",
        "description": "The same document as `/openapi/sites-delivery.yaml`, as JSON. No authentication; `Access-Control-Allow-Origin: *`;\n`Cache-Control: public, max-age=300`.",
        "operationId": "getSitesDeliveryOpenAPIJSON",
        "responses": {
          "200": {
            "description": "The document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "ViewStream Sites Delivery API"
                  },
                  "paths": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi.he.yaml": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "This API as an OpenAPI 3.1 document with Hebrew summaries (YAML)",
        "description": "The document at `/openapi.yaml` with Hebrew tag descriptions and operation summaries (from `api/i18n/he.yaml`, English\nwhere a text is not translated) and a Hebrew note before the introduction; `info.x-language` is `he`. Paths,\nparameters, schemas, responses and examples are the same as in `/openapi.yaml`. The docs page loads it when its\nlanguage switch is set to עברית. No authentication; `Access-Control-Allow-Origin: *`;\n`Cache-Control: public, max-age=300`.",
        "operationId": "getOpenAPIHebrewYAML",
        "responses": {
          "200": {
            "description": "The document (`application/yaml; charset=utf-8`)",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "ViewStream API",
                    "x-language": "he"
                  },
                  "paths": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi.he.json": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "This API as an OpenAPI 3.1 document with Hebrew summaries (JSON)",
        "description": "The document at `/openapi.json` with Hebrew tag descriptions and operation summaries (from `api/i18n/he.yaml`, English\nwhere a text is not translated) and a Hebrew note before the introduction; `info.x-language` is `he`. Paths,\nparameters, schemas, responses and examples are the same as in `/openapi.json`. The docs page loads it when its\nlanguage switch is set to עברית. No authentication; `Access-Control-Allow-Origin: *`;\n`Cache-Control: public, max-age=300`.",
        "operationId": "getOpenAPIHebrewJSON",
        "responses": {
          "200": {
            "description": "The document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "ViewStream API",
                    "x-language": "he"
                  },
                  "paths": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi/sites-delivery.he.yaml": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "The Sites Delivery API as an OpenAPI 3.1 document with Hebrew summaries (YAML)",
        "description": "The document at `/openapi/sites-delivery.yaml` with Hebrew tag descriptions and operation summaries (from `api/i18n/he.yaml`, English\nwhere a text is not translated) and a Hebrew note before the introduction; `info.x-language` is `he`. Paths,\nparameters, schemas, responses and examples are the same as in `/openapi/sites-delivery.yaml`. The docs page loads it when its\nlanguage switch is set to עברית. No authentication; `Access-Control-Allow-Origin: *`;\n`Cache-Control: public, max-age=300`.",
        "operationId": "getSitesDeliveryOpenAPIHebrewYAML",
        "responses": {
          "200": {
            "description": "The document (`application/yaml; charset=utf-8`)",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "ViewStream Sites Delivery API",
                    "x-language": "he"
                  },
                  "paths": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi/sites-delivery.he.json": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "The Sites Delivery API as an OpenAPI 3.1 document with Hebrew summaries (JSON)",
        "description": "The document at `/openapi/sites-delivery.json` with Hebrew tag descriptions and operation summaries (from `api/i18n/he.yaml`, English\nwhere a text is not translated) and a Hebrew note before the introduction; `info.x-language` is `he`. Paths,\nparameters, schemas, responses and examples are the same as in `/openapi/sites-delivery.json`. The docs page loads it when its\nlanguage switch is set to עברית. No authentication; `Access-Control-Allow-Origin: *`;\n`Cache-Control: public, max-age=300`.",
        "operationId": "getSitesDeliveryOpenAPIHebrewJSON",
        "responses": {
          "200": {
            "description": "The document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "ViewStream Sites Delivery API",
                    "x-language": "he"
                  },
                  "paths": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/healthz": {
      "get": {
        "tags": [
          "health"
        ],
        "summary": "Liveness",
        "description": "Answers `200 {\"status\":\"ok\"}` whenever the API process serves requests. It checks no dependency (use it for\nliveness probes and uptime checks); `Cache-Control: no-store`. No authentication.",
        "operationId": "healthz",
        "responses": {
          "200": {
            "description": "Process is up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    }
                  }
                },
                "example": {
                  "status": "ok"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "The caller — user, tenants, current tenant and effective scopes",
        "description": "**Required scope:** none — any valid API key of the tenant.\n\nWho is calling. With an API key: `auth: api_key`, `user: null`, the key's tenant as both `tenants[0]` and\n`current_tenant` (role `api_key`) and the key's scopes. With a Studio session: `auth: session`, the user, every\ntenant of an active membership with the role there, the session's current tenant (null until one is chosen)\nand the scopes of that role. Any valid credential may call it (no scope needed). Use it to check which tenant\nand scopes a key carries before calling other routes.",
        "operationId": "me",
        "responses": {
          "200": {
            "description": "Caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "example": {
                  "auth": "session",
                  "user": {
                    "id": "019286a2-4c33-7c4b-9a1b-3c5d7e9f1a2e",
                    "email": "dana@example.co.il",
                    "name": "Dana Levi",
                    "locale": "he",
                    "tz": "Asia/Jerusalem",
                    "status": "active",
                    "created_at": "2026-09-27T09:00:00Z",
                    "last_login_at": "2026-10-06T07:45:12Z"
                  },
                  "tenants": [
                    {
                      "id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "slug": "now14poc",
                      "name": "Channel 14 PoC",
                      "mode": "platform",
                      "cdn_hostname": "cdn.now14-poc.vustream.net",
                      "role": "editor"
                    }
                  ],
                  "current_tenant": {
                    "id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "slug": "now14poc",
                    "name": "Channel 14 PoC",
                    "mode": "platform",
                    "cdn_hostname": "cdn.now14-poc.vustream.net",
                    "role": "editor"
                  },
                  "scopes": [
                    "assets:read",
                    "channels:read",
                    "clips:read",
                    "stats:read",
                    "events:read",
                    "sites:read",
                    "assets:write",
                    "clips:write",
                    "uploads",
                    "sites:write",
                    "epg:write"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tenants": {
      "get": {
        "tags": [
          "tenants"
        ],
        "summary": "Tenants the caller belongs to (the key's tenant, or the session's memberships)",
        "description": "**Required scope:** none — any valid API key of the tenant.\n\nWith an API key: exactly one item, the key's tenant, with role `api_key`. With a Studio session: one item per\nactive membership with the role there (oldest membership first); Interhost operators get every tenant. No scope\nneeded; any valid credential may call it.",
        "operationId": "listTenants",
        "responses": {
          "200": {
            "description": "Tenants",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TenantSummary"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "slug": "now14poc",
                      "name": "Channel 14 PoC",
                      "mode": "platform",
                      "cdn_hostname": "cdn.now14-poc.vustream.net",
                      "role": "api_key"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/assets": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "List assets (cursor pagination, newest first)",
        "description": "**Required scope:** `assets:read`\n\nThe tenant's assets, newest first (`created_at`, then `id`), as summaries without renditions or jobs — call\n`GET /v1/assets/{id}` for the full object. Trashed assets (`deleted`) are hidden unless you ask for\n`status=deleted`. Filters combine: `status`, `collection` (+ `children`) and the text search `q`. Pass\n`next_cursor` back as `cursor` until it is null. Scope `assets:read`; not available to CDN-only tenants\n(403 `feature_disabled`).",
        "operationId": "listAssets",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Only assets in this state. Without it, every state except `deleted` (the trash) is listed.",
            "schema": {
              "type": "string",
              "enum": [
                "registered",
                "probing",
                "queued",
                "encoding",
                "packaging",
                "ready",
                "failed",
                "deleted"
              ]
            }
          },
          {
            "name": "collection",
            "in": "query",
            "description": "Only assets in this library section of the tenant; an unknown or foreign section is a 422.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "children",
            "in": "query",
            "description": "With `collection`, also assets in its direct sub-sections (`true`; anything else = false)",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Search (Studio Library): only assets whose title, external id, id, description or content summary (summary text, presenters, topics) contains this text, case-insensitively; 2–100 characters after trimming. Each item then carries `match`. Combines with the other filters and the cursor. 503 `not_ready` when the store has no search support.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "`next_cursor` of the previous page (`<created_at RFC 3339>|<id>`); an unparsable cursor is ignored (first page)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of assets; `next_cursor` is null on the last page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetPage"
                },
                "example": {
                  "items": [
                    {
                      "id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                      "external_id": "cms-48211",
                      "title": "Evening news",
                      "status": "ready",
                      "ladder": "news-720p",
                      "duration_ms": 1785600,
                      "published_at": "2026-09-30T18:22:00Z",
                      "ready_at": "2026-09-30T18:21:40Z",
                      "created_at": "2026-09-30T18:05:27Z",
                      "updated_at": "2026-09-30T18:22:00Z",
                      "deleted_at": null
                    }
                  ],
                  "next_cursor": "2026-09-30T18:05:27.420478Z|0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Invalid query: `limit` outside 1–200, unknown `status`, unknown `collection`, or `q` too short/long",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "invalid query",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "errors": [
                    {
                      "field": "limit",
                      "detail": "must be an integer between 1 and 200"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`q` was given but text search is not available (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Register an asset (S3 key under the tenant's ingest prefix, or a URL) and start its probe job",
        "description": "**Required scope:** `assets:write`\n\nRegisters a source that is already in object storage (`source.kind: s3`, a key under `<tenant>/in/` of the\ningest bucket) or that the worker fetches (`url`, http(s) on port 80/443/8443 without credentials; hosts that\nresolve to private, loopback or other non-public addresses are refused — SSRF guard). For browser/file uploads use `POST /v1/uploads` instead. The asset is created with a\n`probe` job and answered as `probing`; it then moves through `queued` → `encoding` → `packaging` → `ready` (or\n`failed`) — follow it with `GET /v1/assets/{id}`, the SSE stream or webhooks (`asset.status`, `asset.ready`).\n`publish: manual` keeps a ready asset unpublished until `PATCH {publish: true}`. Body max 64 KB. Scope\n`assets:write`; not for CDN-only tenants. Audited as `asset.create`; emits `asset.status`.",
        "operationId": "createAsset",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssetCreate"
              },
              "example": {
                "external_id": "cms-48211",
                "title": "Evening news 27.9",
                "source": {
                  "kind": "s3",
                  "key": "now14poc/in/evening-2026-09-27.mov"
                },
                "ladder": "news-1080p",
                "publish": "auto",
                "metadata": {
                  "description": "Main evening edition",
                  "ads": {
                    "cues": [
                      600,
                      1200
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Asset registered (status `probing`) with its probe job",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Asset"
                },
                "example": {
                  "id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "external_id": "cms-48211",
                  "title": "Evening news 27.9",
                  "status": "probing",
                  "source": {
                    "kind": "s3",
                    "bucket": "ingest",
                    "key": "now14poc/in/evening-2026-09-27.mov"
                  },
                  "master_key": null,
                  "ladder": "news-1080p",
                  "probe": null,
                  "duration_ms": null,
                  "error": null,
                  "metadata": {
                    "description": "Main evening edition",
                    "ads": {
                      "cues": [
                        600,
                        1200
                      ]
                    }
                  },
                  "published_at": null,
                  "ready_at": null,
                  "created_at": "2026-09-27T19:02:11Z",
                  "updated_at": "2026-09-27T19:02:11Z",
                  "published": false,
                  "renditions": [],
                  "playback": null,
                  "jobs": [
                    {
                      "id": "0192c8a0-5000-7000-8000-000000000001",
                      "type": "probe",
                      "status": "queued",
                      "attempts": 0,
                      "created_at": "2026-09-27T19:02:11Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 64 KB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "An asset with that external_id exists for this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed: `source.kind`/`source.key`/`source.url` (incl. the SSRF guard), `external_id`, `title`, `metadata` (incl. `metadata.ads`), `publish` or an unknown `ladder`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "invalid asset",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "errors": [
                    {
                      "field": "source.key",
                      "detail": "must be under ingest/now14poc/in/ and contain no traversal"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "Asset with renditions, playback URLs and job summary",
        "description": "**Required scope:** `assets:read`\n\nThe full asset: the stored row, `published`, its renditions, the last 50 jobs (newest first; job errors are\nredacted) and — only while `ready` — the playback URLs on the tenant's CDN hostname (a chosen smart poster\nreplaces the generated one). Trashed (`deleted`) assets and other tenants' assets answer 404; see\n`GET /v1/trash`. Scope `assets:read`; not for CDN-only tenants.",
        "operationId": "getAsset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The asset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Asset"
                },
                "example": {
                  "id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "external_id": "cms-48211",
                  "title": "Evening news",
                  "status": "ready",
                  "source": {
                    "kind": "upload",
                    "bucket": "ingest",
                    "key": "tv10poc/in/0192c8a0-6000-7000-8000-000000000002/evening.mp4"
                  },
                  "master_key": "tv10poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/evening.mp4",
                  "ladder": "news-540p",
                  "probe": {
                    "width": 960,
                    "height": 540,
                    "fps": 25,
                    "duration_ms": 178560,
                    "video_codec": "h264",
                    "audio_codec": "aac",
                    "interlaced": false
                  },
                  "duration_ms": 178560,
                  "error": null,
                  "metadata": {
                    "description": "Main evening edition"
                  },
                  "published_at": "2026-09-30T18:22:00Z",
                  "ready_at": "2026-09-30T18:21:40Z",
                  "created_at": "2026-09-30T18:05:27Z",
                  "updated_at": "2026-09-30T18:22:00Z",
                  "published": true,
                  "playback": {
                    "hls": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/master.m3u8",
                    "poster": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/poster.jpg",
                    "sprite": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/sprite.jpg",
                    "thumbs_vtt": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/thumbs.vtt",
                    "download": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/download.mp4"
                  },
                  "renditions": [
                    {
                      "id": "0192c8a0-5100-7000-8000-000000000011",
                      "asset_id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                      "kind": "video",
                      "label": "540p",
                      "width": 960,
                      "height": 540,
                      "bitrate_kbps": 1053,
                      "codec": "avc1",
                      "s3_key": "tv10poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/540p.m3u8",
                      "size_bytes": 23519955,
                      "created_at": "2026-09-30T18:21:40Z"
                    },
                    {
                      "id": "0192c8a0-5100-7000-8000-000000000012",
                      "asset_id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                      "kind": "audio",
                      "label": "aac-128",
                      "bitrate_kbps": 132,
                      "codec": "mp4a.40.2",
                      "s3_key": "tv10poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/aac-128.m3u8",
                      "size_bytes": 2969509,
                      "created_at": "2026-09-30T18:21:40Z"
                    }
                  ],
                  "jobs": [
                    {
                      "id": "0192c8a0-5000-7000-8000-000000000001",
                      "type": "probe",
                      "status": "succeeded",
                      "attempts": 1,
                      "created_at": "2026-09-30T18:05:27Z",
                      "finished_at": "2026-09-30T18:05:31Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset in this tenant, or it is in the trash",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      },
      "patch": {
        "tags": [
          "assets"
        ],
        "summary": "Update title, external_id, metadata; publish/unpublish (needs assets:publish)",
        "description": "**Required scope:** `assets:write`\n\nPartial update: only the fields present change. `metadata` replaces the whole object (no merge).\n`external_id: \"\"` clears it. `publish` needs scope `assets:publish` (403 otherwise); `true` needs a `ready`\nasset and keeps the first `published_at`, `false` clears it. `policy_id` attaches a playback policy to the\nasset (or `null` = inherit the tenant's) and needs scope `delivery:write`; a policy change can queue\nre-encodes of the tenant's assets when the encryption mode changes. Body max 64 KB. Scope `assets:write`; not\nfor CDN-only tenants; trashed assets are 404. Audited as `asset.update`; the first publish emits\n`asset.published`.",
        "operationId": "patchAsset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "external_id": {
                    "type": "string",
                    "pattern": "^$|^[A-Za-z0-9._:/-]{1,200}$",
                    "description": "Empty string clears it; unique per tenant (409)"
                  },
                  "metadata": {
                    "type": "object",
                    "description": "Replaces the whole metadata object; max 16 KB; `metadata.ads` is validated as on create; keys starting with _ are reserved"
                  },
                  "publish": {
                    "type": "boolean",
                    "description": "true sets published_at (asset must be ready), false clears it; needs scope assets:publish"
                  },
                  "policy_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Attach a playback policy of the tenant to this asset; null detaches (inherit). Needs scope delivery:write"
                  }
                }
              },
              "example": {
                "title": "Evening news 27.9 (full)",
                "metadata": {
                  "description": "Main evening edition",
                  "ads": {
                    "cues": [
                      600
                    ]
                  }
                },
                "publish": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated asset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Asset"
                },
                "example": {
                  "id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "external_id": "cms-48211",
                  "title": "Evening news 27.9 (full)",
                  "status": "ready",
                  "ladder": "news-1080p",
                  "duration_ms": 1785600,
                  "metadata": {
                    "description": "Main evening edition",
                    "ads": {
                      "cues": [
                        600
                      ]
                    }
                  },
                  "published_at": "2026-09-30T18:22:00Z",
                  "ready_at": "2026-09-30T18:21:40Z",
                  "created_at": "2026-09-30T18:05:27Z",
                  "updated_at": "2026-09-30T18:22:00Z",
                  "published": true,
                  "playback": {
                    "hls": "https://cdn.now14-poc.vustream.net/vod/now14poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/master.m3u8"
                  },
                  "renditions": [],
                  "jobs": []
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 64 KB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing scope (`assets:write`; `assets:publish` for `publish`; `delivery:write` for `policy_id`), CDN-only tenant, or suspended tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such asset (or it is in the trash), or `policy_id` names no policy of this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Publish requested on an asset that is not ready, or external_id in use",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      },
      "delete": {
        "tags": [
          "assets"
        ],
        "summary": "Delete an asset (marks `deleted` = moves it to the trash, purges vod/; masters kept unless ?masters=true)",
        "description": "**Required scope:** `assets:write`\n\nDelete an asset (marks `deleted` = moves it to the trash, purges vod/; masters kept unless ?masters=true). With ?permanent=true a trashed asset is deleted for good (scope assets:purge)\n\nWithout `permanent`: moves a live asset to the trash — status `deleted`, unpublished, and a `purge` job (bulk\npriority) that removes its renditions under vod/ and the edge copies; the master stays unless `masters=true`\n(without a master the asset cannot be restored). Answers 204. Audited as `asset.delete`; emits `asset.status`.\nRestore with `POST /v1/assets/{id}/restore` until the trash retention purges it.\n\nWith `permanent=true` (scope `assets:purge`, checked in the handler): the asset must already be in the trash\n(409 otherwise). Queues a permanent purge job — masters, ingest source, renditions, edge copies and the row —\nand answers 202 with it; a second call returns the job already pending (idempotent). Audited as\n`asset.purge_requested`. Scope `assets:write`; not for CDN-only tenants.",
        "operationId": "deleteAsset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "masters",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Move to the trash: `true` also deletes the master (the asset then cannot be restored). Ignored with `permanent`"
          },
          {
            "name": "permanent",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "`true`: delete a trashed asset permanently: masters, ingest source, renditions, edge copies and the row (scope assets:purge; 202 + job)"
          }
        ],
        "responses": {
          "202": {
            "description": "`permanent=true`: permanent delete queued (or the pending one returned)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job"
                  ],
                  "properties": {
                    "job": {
                      "$ref": "#/components/schemas/Job"
                    }
                  }
                },
                "example": {
                  "job": {
                    "id": "0192c8a0-5000-7000-8000-00000000000a",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "type": "purge",
                    "priority": 3,
                    "status": "queued",
                    "attempts": 0,
                    "max_attempts": 3,
                    "asset_id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                    "created_at": "2026-10-06T09:12:00Z"
                  }
                }
              }
            }
          },
          "204": {
            "description": "Moved to the trash; a purge job for its renditions was created"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing scope (`assets:write`; `assets:purge` with `permanent=true`), CDN-only tenant, or suspended tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such asset in this tenant (without `permanent`, a trashed asset is also 404)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Already deleted (a concurrent delete), or with `permanent=true` the asset is not in the trash",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}/restore": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Restore a trashed asset",
        "description": "**Required scope:** `assets:write`\n\nRestore a trashed asset — rebuilt from its master with its ladder; it waits in `registered` and comes back `ready`, unpublished\n\nTakes an asset out of the trash by re-encoding its master with its own ladder (a `reencode` job at fast\npriority 1, encrypted as its playback policy asks). The asset moves to `registered` at once and becomes\n`ready` — unpublished — when the job finishes; publish it again with `PATCH {publish: true}`. Needs the master\nin storage (`restorable` in `GET /v1/trash`) and no pending permanent delete. Async: 202 with the job. Scope\n`assets:write`; not for CDN-only tenants. Audited as `asset.restore`; emits `asset.status`.",
        "operationId": "restoreAsset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the trashed asset",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Restore (reencode) job created; the asset is `registered`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job",
                    "status"
                  ],
                  "properties": {
                    "job": {
                      "$ref": "#/components/schemas/Job"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "registered"
                      ]
                    }
                  }
                },
                "example": {
                  "job": {
                    "id": "0192c8a0-5000-7000-8000-00000000000b",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "type": "reencode",
                    "priority": 1,
                    "status": "queued",
                    "attempts": 0,
                    "max_attempts": 3,
                    "asset_id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                    "created_at": "2026-10-06T09:15:00Z"
                  },
                  "status": "registered"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Not in the trash, master gone, ladder profile gone, a permanent delete is pending, encryption keys unavailable, or it left the trash meanwhile",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/trash": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "The tenant's trash — deleted assets with the bytes they still hold, whether they can be restored and when…",
        "description": "**Required scope:** `assets:read`\n\nThe tenant's trash — deleted assets with the bytes they still hold, whether they can be restored and when they are purged automatically\n\nTrashed assets newest first (at most 1000 are walked; `truncated` says more exist), each with the bytes it still\nholds in storage (renditions left under vod/, the master, the ingest source), `restorable` (the master exists\nand no permanent delete is pending) and `purge_at` = `deleted_at` + `retention_days`. Also lists the channels\nin the trash, which Empty trash does not touch. Sizes are read live from object storage,\nso this call can be slow with a large trash. Scope `assets:read`; not for CDN-only tenants.",
        "operationId": "getTrash",
        "responses": {
          "200": {
            "description": "Trash summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trash"
                },
                "example": {
                  "count": 1,
                  "bytes": 182544311,
                  "retention_days": 30,
                  "truncated": false,
                  "items": [
                    {
                      "id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                      "title": "Evening news 27.9",
                      "deleted_at": "2026-10-01T08:00:00Z",
                      "purge_at": "2026-10-31T08:00:00Z",
                      "bytes": 182544311,
                      "restorable": true,
                      "purge_pending": false
                    }
                  ],
                  "channels": [
                    {
                      "id": "019a0000-0000-7000-8000-0000000000c2",
                      "slug": "test-feed",
                      "title": "Test feed",
                      "deleted_at": "2026-10-02T10:00:00Z",
                      "purge_at": "2026-11-01T10:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/trash/empty": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Empty the trash — a permanent purge job per trashed asset (idempotent; returns counts and bytes)",
        "description": "**Required scope:** `assets:purge`\n\nQueues a permanent purge job (masters, ingest source, renditions, edge copies and the row) for every trashed\nasset (up to 1000 per call; `truncated` = call again). Assets that already have a pending permanent delete are\ncounted in `already_pending` and not queued twice, so repeating the call is safe. `bytes` is what the newly\nqueued assets hold. Channels in the trash are not touched (they are purged at their `purge_at`). No body.\nScope `assets:purge`; not for CDN-only tenants. Audited as `trash.empty`.",
        "operationId": "emptyTrash",
        "responses": {
          "202": {
            "description": "Purges queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "queued",
                    "already_pending",
                    "bytes",
                    "truncated"
                  ],
                  "properties": {
                    "queued": {
                      "type": "integer",
                      "description": "Permanent purge jobs created by this call"
                    },
                    "already_pending": {
                      "type": "integer",
                      "description": "Trashed assets whose permanent purge was already queued"
                    },
                    "bytes": {
                      "type": "integer",
                      "format": "int64",
                      "description": "Storage the newly queued assets hold"
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "The trash holds more than 1000 assets; call again"
                    }
                  }
                },
                "example": {
                  "queued": 3,
                  "already_pending": 1,
                  "bytes": 1204318822,
                  "truncated": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:purge"
      }
    },
    "/v1/trash/settings": {
      "patch": {
        "tags": [
          "assets"
        ],
        "summary": "Set how many days a trashed asset is kept before it is purged automatically (1–365, default 30)",
        "description": "**Required scope:** `assets:purge`\n\nStores `retention_days` in the tenant settings (`trash_retention_days`); the automatic purge and `purge_at`\nin `GET /v1/trash` use it from now on. Body max 1 KB. Scope `assets:purge`; not for CDN-only tenants. Audited\nas `trash.settings`.",
        "operationId": "patchTrashSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "retention_days"
                ],
                "additionalProperties": false,
                "properties": {
                  "retention_days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365
                  }
                }
              },
              "example": {
                "retention_days": 14
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the retention now in effect",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "retention_days"
                  ],
                  "properties": {
                    "retention_days": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "retention_days": 14
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON or an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`retention_days` missing or outside 1–365",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:purge"
      }
    },
    "/v1/library/imports": {
      "get": {
        "tags": [
          "library-imports"
        ],
        "summary": "Library imports from a sitemap, newest first (50)",
        "description": "**Required scope:** `assets:read`\n\nThe tenant's 50 newest library imports (no pagination), each with its discovery statistics and item counts.\nNot available to CDN-only tenants (403 `feature_disabled`). Scope `assets:read`.",
        "operationId": "listLibraryImports",
        "responses": {
          "200": {
            "description": "The newest imports, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "maxItems": 50,
                      "items": {
                        "$ref": "#/components/schemas/LibraryImport"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0199b9e2-3c41-7f05-9a6e-2d8c4b1f7e30",
                      "sitemap_url": "https://www.example.co.il/video-sitemap.xml",
                      "status": "discovered",
                      "options": {
                        "extract_pages": false,
                        "max_items": 1000
                      },
                      "import_options": null,
                      "resync": false,
                      "next_sync_at": null,
                      "last_sync_at": null,
                      "error": null,
                      "created_by": "key:k7Qm2xPa",
                      "created_at": "2026-10-05T09:14:02Z",
                      "updated_at": "2026-10-05T09:16:40Z",
                      "finished_at": "2026-10-05T09:16:40Z",
                      "stats": {
                        "sitemaps": 3,
                        "pages": 0,
                        "requests": 4,
                        "robots_blocked": 0,
                        "truncated": false,
                        "errors": []
                      },
                      "counts": {
                        "total": 412,
                        "mp4": 377,
                        "hls": 21,
                        "unsupported": 14,
                        "duplicates": 6,
                        "discovered": 412,
                        "queued": 0,
                        "fetching": 0,
                        "processing": 0,
                        "imported": 0,
                        "failed": 0,
                        "skipped": 0,
                        "cancelled": 0
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "post": {
        "tags": [
          "library-imports"
        ],
        "summary": "Discover the videos of a sitemap (index, plain, Google video sitemap, gzip; optionally the video of each page)",
        "description": "**Required scope:** `assets:write`\n\nDiscover the videos of a sitemap (index, plain, Google video sitemap, gzip; optionally the video of each page). Runs in the background (2 requests/s per host, robots.txt respected); poll the import, then read its items. The URL must be public http(s): private, loopback, link-local and metadata addresses are refused, also after redirects.\n\nCreates an import in status `discovering` and answers 202 at once; the crawler (orchestrator leader) reads the\nsitemap, sitemap index (up to 3 levels, 200 files), `.xml.gz` or robots.txt `Sitemap:` lines in the background,\nthen the import becomes `discovered` (or `failed`). Poll `GET /v1/library/imports/{id}` and preview the items\nwith `GET …/items`; nothing is imported until `POST …/start`. At most 3 imports of a tenant may be discovering at\nonce (429, `Retry-After: 60`). The URL is checked against the SSRF policy before anything is fetched (422 with\nthe reason). Unknown body fields are rejected (400). Audited as `library_import.create`. Not available to CDN-only tenants (403 `feature_disabled`). Scope\n`assets:write`.",
        "operationId": "createLibraryImport",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sitemap_url"
                ],
                "additionalProperties": false,
                "properties": {
                  "sitemap_url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "Absolute http(s) URL of a sitemap, sitemap index,.xml.gz or robots.txt; trimmed"
                  },
                  "extract_pages": {
                    "type": "boolean",
                    "default": false,
                    "description": "Open each page URL that is not itself a video and look for og:video, JSON-LD VideoObject, <video>/<source> or.m3u8/.mp4 links"
                  },
                  "max_items": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10000,
                    "default": 1000,
                    "description": "Stop discovering after this many items (`stats.truncated` = true)"
                  }
                }
              },
              "example": {
                "sitemap_url": "https://www.example.co.il/video-sitemap.xml",
                "extract_pages": false,
                "max_items": 1000
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Discovery started (status `discovering`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryImport"
                },
                "example": {
                  "id": "0199b9e2-3c41-7f05-9a6e-2d8c4b1f7e30",
                  "sitemap_url": "https://www.example.co.il/video-sitemap.xml",
                  "status": "discovering",
                  "options": {
                    "extract_pages": false,
                    "max_items": 1000
                  },
                  "import_options": null,
                  "resync": false,
                  "next_sync_at": null,
                  "last_sync_at": null,
                  "error": null,
                  "created_by": "key:k7Qm2xPa",
                  "created_at": "2026-10-05T09:14:02Z",
                  "updated_at": "2026-10-05T09:14:02Z",
                  "finished_at": null,
                  "stats": {
                    "sitemaps": 3,
                    "pages": 0,
                    "requests": 4,
                    "robots_blocked": 0,
                    "truncated": false,
                    "errors": []
                  },
                  "counts": {
                    "total": 0,
                    "mp4": 0,
                    "hls": 0,
                    "unsupported": 0,
                    "duplicates": 0,
                    "discovered": 0,
                    "queued": 0,
                    "fetching": 0,
                    "processing": 0,
                    "imported": 0,
                    "failed": 0,
                    "skipped": 0,
                    "cancelled": 0
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 8 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Validation failed: `sitemap_url` not an absolute http(s) URL / too long / refused by the fetch policy (private or reserved address), or `max_items` outside 1–10000",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "3 discoveries are already running for this tenant (`rate_limited`); retry after `Retry-After` seconds (60)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/library/imports/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "library-imports"
        ],
        "summary": "One import with discovery statistics and item counts by status and source type",
        "description": "**Required scope:** `assets:read`\n\nOne import with `stats` (discovery progress: sitemaps read, pages opened, requests, robots.txt blocks, the first\n20 fetch errors) and `counts` (items by source type, duplicates and import status). Poll this while\n`status` is `discovering` or `importing`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:read`.",
        "operationId": "getLibraryImport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Import id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The import",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryImport"
                },
                "example": {
                  "id": "0199b9e2-3c41-7f05-9a6e-2d8c4b1f7e30",
                  "sitemap_url": "https://www.example.co.il/video-sitemap.xml",
                  "status": "importing",
                  "options": {
                    "extract_pages": false,
                    "max_items": 1000
                  },
                  "import_options": {
                    "publish": "manual",
                    "subtitles": true,
                    "filters": {
                      "from": "2026-01-01",
                      "path_prefix": "/news/"
                    }
                  },
                  "resync": false,
                  "next_sync_at": null,
                  "last_sync_at": null,
                  "error": null,
                  "created_by": "key:k7Qm2xPa",
                  "created_at": "2026-10-05T09:14:02Z",
                  "updated_at": "2026-10-05T09:20:11Z",
                  "finished_at": null,
                  "stats": {
                    "sitemaps": 3,
                    "pages": 0,
                    "requests": 4,
                    "robots_blocked": 0,
                    "truncated": false,
                    "errors": []
                  },
                  "counts": {
                    "total": 412,
                    "mp4": 377,
                    "hls": 21,
                    "unsupported": 14,
                    "duplicates": 6,
                    "discovered": 362,
                    "queued": 44,
                    "fetching": 3,
                    "processing": 3,
                    "imported": 0,
                    "failed": 0,
                    "skipped": 0,
                    "cancelled": 0
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such import for this tenant (`not_found`; also for an id that is not a UUID)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "patch": {
        "tags": [
          "library-imports"
        ],
        "summary": "Turn the daily re-sync on or off (new items of the same sitemap are imported with the options and filters of the start). Only after the import was started.",
        "description": "**Required scope:** `assets:write`\n\nWith `resync: true` the import is re-discovered every 24 hours (first run 24 h from now, `next_sync_at`); items\nfirst seen in a re-sync that are supported and not duplicates are queued with the publish/subtitle options and\nfilters of the last start. Turning it on before the import was started is a 409. `resync: false` clears\n`next_sync_at`. A body that is not exactly `{\"resync\": true|false}` is a 422. Audited as\n`library_import.resync`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "patchLibraryImport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Import id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "resync"
                ],
                "additionalProperties": false,
                "properties": {
                  "resync": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "resync": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the import",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryImport"
                },
                "example": {
                  "id": "0199b9e2-3c41-7f05-9a6e-2d8c4b1f7e30",
                  "sitemap_url": "https://www.example.co.il/video-sitemap.xml",
                  "status": "importing",
                  "options": {
                    "extract_pages": false,
                    "max_items": 1000
                  },
                  "import_options": {
                    "publish": "manual",
                    "subtitles": true,
                    "filters": {
                      "from": "2026-01-01",
                      "path_prefix": "/news/"
                    }
                  },
                  "resync": true,
                  "next_sync_at": "2026-10-06T09:31:55Z",
                  "last_sync_at": null,
                  "error": null,
                  "created_by": "key:k7Qm2xPa",
                  "created_at": "2026-10-05T09:14:02Z",
                  "updated_at": "2026-10-05T09:31:55Z",
                  "finished_at": null,
                  "stats": {
                    "sitemaps": 3,
                    "pages": 0,
                    "requests": 4,
                    "robots_blocked": 0,
                    "truncated": false,
                    "errors": []
                  },
                  "counts": {
                    "total": 412,
                    "mp4": 377,
                    "hls": 21,
                    "unsupported": 14,
                    "duplicates": 6,
                    "discovered": 362,
                    "queued": 44,
                    "fetching": 3,
                    "processing": 3,
                    "imported": 0,
                    "failed": 0,
                    "skipped": 0,
                    "cancelled": 0
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such import for this tenant (`not_found`; also for an id that is not a UUID)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`resync: true` before the import was started (`conflict`: start the import first)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Body missing, not JSON, with other fields, or `resync` not a boolean (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      },
      "delete": {
        "tags": [
          "library-imports"
        ],
        "summary": "Delete the import record and its items (imported assets stay in the library)",
        "description": "**Required scope:** `assets:write`\n\nDelete the import record and its items (imported assets stay in the library). Not while it is discovering or has items in flight: cancel first.\n\nDeletes the import and all its items; assets created by it stay in the library. Refused with 409 while the\nimport is `discovering` or has items `queued`, `fetching` or `processing` — cancel it first. Audited as\n`library_import.delete`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "deleteLibraryImport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Import id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such import for this tenant (`not_found`; also for an id that is not a UUID)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The import is running (`conflict`: cancel it first)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/library/imports/{id}/items": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "library-imports"
        ],
        "summary": "The discovered videos (preview) with per-item import status; newest publication date first",
        "description": "**Required scope:** `assets:read`\n\nItems of one import, ordered by `published_at` (newest first, items without a date last), with the total that\nmatches the filters. Use it as the preview before `start` and to follow per-item progress afterwards.\nOffset-based cursor pagination: pass `next_cursor` back as `cursor` (null = last page). Filters combine (AND).\nA bad filter value is a 422 listing the field. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:read`.",
        "operationId": "listLibraryImportItems",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Import id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only items in this import status",
            "schema": {
              "type": "string",
              "enum": [
                "discovered",
                "queued",
                "fetching",
                "processing",
                "imported",
                "failed",
                "skipped",
                "cancelled"
              ]
            }
          },
          {
            "name": "source_type",
            "in": "query",
            "description": "Comma-separated list of mp4, hls, unsupported",
            "schema": {
              "type": "string"
            },
            "example": "mp4,hls"
          },
          {
            "name": "duplicate",
            "in": "query",
            "description": "true = only items whose source is already a non-deleted asset of the tenant; false = only the others",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Published at or after (RFC 3339, or YYYY-MM-DD = start of that day in Asia/Jerusalem)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Published at or before (RFC 3339, or YYYY-MM-DD = end of that day in Asia/Jerusalem)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path_prefix",
            "in": "query",
            "description": "The page URL path (or the media URL path when there is no page) starts with this, e.g. /news/; must start with / (≤ 512)",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Title contains (case-insensitive; cut to 200 characters)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "`next_cursor` of the previous page",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "total",
                    "next_cursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LibraryImportItem"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Items matching the filters (all pages)"
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Pass as `cursor` for the next page; null on the last page"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0199b9e3-01a7-7b22-8c4d-5e6f7a8b9c0d",
                      "source_url": "https://media.example.co.il/vod/2026/10/04/evening-news.mp4",
                      "page_url": "https://www.example.co.il/news/evening-2026-10-04",
                      "title": "Evening news 4.10",
                      "description": "The evening edition",
                      "thumbnail_url": "https://media.example.co.il/thumbs/evening-2026-10-04.jpg",
                      "duration_s": 1786,
                      "published_at": "2026-10-04T18:00:00Z",
                      "tags": [
                        "news"
                      ],
                      "source_type": "mp4",
                      "via": "video_sitemap",
                      "note": null,
                      "status": "imported",
                      "duplicate": false,
                      "asset_id": "0199b9f0-6a12-7e3b-9d48-1c2b3a4d5e6f",
                      "asset_status": "ready",
                      "error": null,
                      "attempts": 1,
                      "updated_at": "2026-10-05T09:42:18Z"
                    }
                  ],
                  "total": 412,
                  "next_cursor": "bzoxMDA"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such import for this tenant (`not_found`; also for an id that is not a UUID)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A filter is invalid (`errors[]`: status, source_type, duplicate, from, to, path_prefix, cursor or limit)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/library/imports/{id}/start": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "library-imports"
        ],
        "summary": "Import the chosen items",
        "description": "**Required scope:** `assets:write`\n\nImport the chosen items: explicit item_ids, or every item matching the filters (supported, not duplicates unless asked), newest first up to limit. Creates the assets and fetches the media in the background (3 items of an import in flight at a time). Idempotent: items already queued or imported are skipped.\n\nQueues the selection and moves the import to `importing` (202). Selected are items in status `discovered`,\n`cancelled` or `skipped` that match `filters` (and `item_ids` when given), are mp4/HLS and not duplicates\n(unless `filters.include_duplicates`), newest publication first, up to `limit` (default/max 10000). Explicit\n`item_ids` that are unsupported or duplicates are marked `skipped`. The background runner then creates one asset\nper item (title from the sitemap; description, tags, publication date and `import` provenance in its metadata) with an\n`import_fetch` job, at most 3 items of an import in flight. 409 while discovery is still running; 422 when\nnothing in the selection can be imported. The publish/subtitle options and filters are kept for re-sync.\nAudited as `library_import.start`; assets emit the usual `asset.status` events. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "startLibraryImport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Import id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "item_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "maxItems": 10000,
                    "description": "Only these items (still subject to filters and status)"
                  },
                  "filters": {
                    "$ref": "#/components/schemas/LibraryImportFilters"
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10000,
                    "description": "At most this many items (newest first); default all, capped at 10000"
                  },
                  "publish": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "manual"
                    ],
                    "default": "auto",
                    "description": "auto = the tenant's auto-publish setting applies when ready; manual = stays a draft"
                  },
                  "subtitles": {
                    "type": "boolean",
                    "default": true,
                    "description": "false = no automatic subtitles for these assets even when the tenant has them on"
                  }
                }
              },
              "example": {
                "filters": {
                  "from": "2026-01-01",
                  "path_prefix": "/news/"
                },
                "limit": 50,
                "publish": "manual",
                "subtitles": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Import started (status `importing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryImport"
                },
                "example": {
                  "id": "0199b9e2-3c41-7f05-9a6e-2d8c4b1f7e30",
                  "sitemap_url": "https://www.example.co.il/video-sitemap.xml",
                  "status": "importing",
                  "options": {
                    "extract_pages": false,
                    "max_items": 1000
                  },
                  "import_options": {
                    "publish": "manual",
                    "subtitles": true,
                    "filters": {
                      "from": "2026-01-01",
                      "path_prefix": "/news/"
                    }
                  },
                  "resync": false,
                  "next_sync_at": null,
                  "last_sync_at": null,
                  "error": null,
                  "created_by": "key:k7Qm2xPa",
                  "created_at": "2026-10-05T09:14:02Z",
                  "updated_at": "2026-10-05T09:20:11Z",
                  "finished_at": null,
                  "stats": {
                    "sitemaps": 3,
                    "pages": 0,
                    "requests": 4,
                    "robots_blocked": 0,
                    "truncated": false,
                    "errors": []
                  },
                  "counts": {
                    "total": 412,
                    "mp4": 377,
                    "hls": 21,
                    "unsupported": 14,
                    "duplicates": 6,
                    "discovered": 362,
                    "queued": 44,
                    "fetching": 3,
                    "processing": 3,
                    "imported": 0,
                    "failed": 0,
                    "skipped": 0,
                    "cancelled": 0
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 512 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such import for this tenant (`not_found`; also for an id that is not a UUID)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Discovery is still running (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid `item_ids` (over 10000), `publish`, `filters.source_types` (mp4 or hls), `filters.from`/`to`, `filters.path_prefix` or `limit`; or nothing to import (no supported item in the selection that is not already in the library or already imported)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/library/imports/{id}/retry": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "library-imports"
        ],
        "summary": "Retry failed items (all failed items when item_ids is empty)",
        "description": "**Required scope:** `assets:write`\n\nRetry failed items (all failed items when item_ids is empty): the asset goes back to registered and a new import_fetch job runs\n\nPuts `failed` items (all of them, or only `item_ids`) back to `queued` and the import to `importing` (unless it\nis discovering); the runner re-fetches each into the same asset. The body is optional. 409 when no failed item\nmatched. Audited as `library_import.retry`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "retryLibraryImport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Import id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "item_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Only these items; empty or omitted = every failed item"
                  }
                }
              },
              "example": {
                "item_ids": [
                  "0199b9e3-01a7-7b22-8c4d-5e6f7a8b9c0d"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Retried; the import",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryImport"
                },
                "example": {
                  "id": "0199b9e2-3c41-7f05-9a6e-2d8c4b1f7e30",
                  "sitemap_url": "https://www.example.co.il/video-sitemap.xml",
                  "status": "importing",
                  "options": {
                    "extract_pages": false,
                    "max_items": 1000
                  },
                  "import_options": {
                    "publish": "manual",
                    "subtitles": true,
                    "filters": {
                      "from": "2026-01-01",
                      "path_prefix": "/news/"
                    }
                  },
                  "resync": false,
                  "next_sync_at": null,
                  "last_sync_at": null,
                  "error": null,
                  "created_by": "key:k7Qm2xPa",
                  "created_at": "2026-10-05T09:14:02Z",
                  "updated_at": "2026-10-05T11:40:03Z",
                  "finished_at": null,
                  "stats": {
                    "sitemaps": 3,
                    "pages": 0,
                    "requests": 4,
                    "robots_blocked": 0,
                    "truncated": false,
                    "errors": []
                  },
                  "counts": {
                    "total": 412,
                    "mp4": 377,
                    "hls": 21,
                    "unsupported": 14,
                    "duplicates": 6,
                    "discovered": 362,
                    "queued": 3,
                    "fetching": 0,
                    "processing": 0,
                    "imported": 45,
                    "failed": 0,
                    "skipped": 0,
                    "cancelled": 2
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 512 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such import for this tenant (`not_found`; also for an id that is not a UUID)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "No failed items to retry (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/library/imports/{id}/cancel": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "library-imports"
        ],
        "summary": "Stop discovery, or stop queueing more items…",
        "description": "**Required scope:** `assets:write`\n\nStop discovery, or stop queueing more items (items already fetching finish; queued ones become cancelled and can be imported again with start)\n\nA `discovering` or `importing` import becomes `cancelled` (other statuses stay as they are), a running\nre-sync stops (the daily `resync` setting is kept), and every `queued` item becomes `cancelled`; items already fetching or processing finish.\nCancelled items can be imported again with `start`. Safe to repeat. Audited as `library_import.cancel`. Not available to CDN-only tenants (403 `feature_disabled`).\nScope `assets:write`.",
        "operationId": "cancelLibraryImport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Import id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled; the import",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryImport"
                },
                "example": {
                  "id": "0199b9e2-3c41-7f05-9a6e-2d8c4b1f7e30",
                  "sitemap_url": "https://www.example.co.il/video-sitemap.xml",
                  "status": "cancelled",
                  "options": {
                    "extract_pages": false,
                    "max_items": 1000
                  },
                  "import_options": {
                    "publish": "manual",
                    "subtitles": true,
                    "filters": {
                      "from": "2026-01-01",
                      "path_prefix": "/news/"
                    }
                  },
                  "resync": false,
                  "next_sync_at": null,
                  "last_sync_at": null,
                  "error": null,
                  "created_by": "key:k7Qm2xPa",
                  "created_at": "2026-10-05T09:14:02Z",
                  "updated_at": "2026-10-05T10:02:37Z",
                  "finished_at": "2026-10-05T10:02:37Z",
                  "stats": {
                    "sitemaps": 3,
                    "pages": 0,
                    "requests": 4,
                    "robots_blocked": 0,
                    "truncated": false,
                    "errors": []
                  },
                  "counts": {
                    "total": 412,
                    "mp4": 377,
                    "hls": 21,
                    "unsupported": 14,
                    "duplicates": 6,
                    "discovered": 362,
                    "queued": 0,
                    "fetching": 0,
                    "processing": 0,
                    "imported": 45,
                    "failed": 1,
                    "skipped": 0,
                    "cancelled": 4
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such import for this tenant (`not_found`; also for an id that is not a UUID)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Library import is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/packaging/settings": {
      "get": {
        "tags": [
          "packaging"
        ],
        "summary": "Output packaging default of the tenant",
        "description": "**Required scope:** `channels:read`\n\nOutput packaging default of the tenant: fmp4 = CMAF/fMP4 only (the platform default, LL-HLS and CMAF DRM); ts = MPEG-TS for the platform-issued URLs; both = fMP4 plus an alternate MPEG-TS URL for compatibility players. TS is remuxed on the fly from the same CMAF segments.\n\nReturns the tenant default as resolved: `mode` is the stored tenant row, or `fmp4` with `source: default` when\nnone is set. `warnings` lists the trade-offs of the effective mode (`ts` loses LL-HLS and multi-DRM and has\nlarger segments; `both` serves its TS alternate without LL-HLS and DRM). `urls` is empty here — channel and\nasset lookups carry the URLs. Platform tenants only (403 `feature_disabled` for CDN-only tenants).",
        "operationId": "getPackagingSettings",
        "responses": {
          "200": {
            "description": "Effective tenant packaging",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                },
                "example": {
                  "mode": "both",
                  "source": "tenant",
                  "tenant": {
                    "mode": "both",
                    "updated_at": "2026-10-01T09:14:22Z"
                  },
                  "urls": {
                    "fmp4": null
                  },
                  "warnings": [
                    "ts_alternate_no_ll_hls",
                    "ts_alternate_no_drm"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — packaging options are not available (store not configured or migration 0028 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "packaging"
        ],
        "summary": "Set the tenant default ({mode: fmp4|ts|both}; null = back to fmp4)",
        "description": "**Required scope:** `tenant:settings`\n\nSets the tenant default output packaging, or clears it with `{\"mode\": null}` (the tenant then resolves to\n`fmp4`, `source: default`). The mode is trimmed and lower-cased before validation. Channel and asset overrides\nare not touched. Answers with the same body as GET. Audited as `packaging.settings`.",
        "operationId": "putPackagingSettings",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 1 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PackagingRequest"
              },
              "example": {
                "mode": "both"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the effective tenant packaging after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                },
                "example": {
                  "mode": "both",
                  "source": "tenant",
                  "tenant": {
                    "mode": "both",
                    "updated_at": "2026-10-06T08:02:11Z"
                  },
                  "urls": {
                    "fmp4": null
                  },
                  "warnings": [
                    "ts_alternate_no_ll_hls",
                    "ts_alternate_no_drm"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 1 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — mode must be fmp4, ts or both",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — packaging options are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/channels/{id}/packaging": {
      "get": {
        "tags": [
          "packaging"
        ],
        "summary": "Effective output packaging of a channel (override → tenant → fmp4) and the live / catch-up URLs it serves",
        "description": "**Required scope:** `channels:read`\n\nResolves the channel's mode: its override if set, else the tenant default, else `fmp4` (`source` says which).\n`urls.fmp4` always has `live` and `catch_up`; `urls.ts` is present only when the mode is `ts` or `both`.\nCatch-up URLs are templates — replace `{start_ms}` and `{end_ms}` with epoch milliseconds. A trashed channel\nis 404.",
        "operationId": "getChannelPackaging",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Effective packaging and URLs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                },
                "example": {
                  "mode": "both",
                  "source": "channel",
                  "tenant": {
                    "mode": null
                  },
                  "channel": {
                    "mode": "both",
                    "updated_at": "2026-10-02T11:40:05Z"
                  },
                  "urls": {
                    "fmp4": {
                      "live": "https://cdn.now14-poc.vustream.net/live/now14poc/main/master.m3u8",
                      "catch_up": "https://cdn.now14-poc.vustream.net/m/catchup/main/{start_ms}/{end_ms}/master.m3u8?c=now14poc"
                    },
                    "ts": {
                      "live": "https://cdn.now14-poc.vustream.net/m/ts/live/now14poc/main/master.m3u8",
                      "catch_up": "https://cdn.now14-poc.vustream.net/m/catchup/main/{start_ms}/{end_ms}/ts/master.m3u8?c=now14poc"
                    }
                  },
                  "warnings": [
                    "ts_alternate_no_ll_hls",
                    "ts_alternate_no_drm"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — no such channel in this tenant (or it is in the trash)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — packaging options are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "packaging"
        ],
        "summary": "Set a channel override ({mode}; null = inherit the tenant)",
        "description": "**Required scope:** `channels:write`\n\nSets the channel's packaging override, or removes it with `{\"mode\": null}` so the channel inherits the tenant\ndefault. TS is remuxed on the fly at the edge, so no re-encode or restart happens. Answers with the same body\nas GET (effective mode + URLs). Audited as `packaging.channel`.",
        "operationId": "putChannelPackaging",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 1 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PackagingRequest"
              },
              "example": {
                "mode": "ts"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the effective channel packaging after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                },
                "example": {
                  "mode": "ts",
                  "source": "channel",
                  "tenant": {
                    "mode": null
                  },
                  "channel": {
                    "mode": "ts",
                    "updated_at": "2026-10-06T08:05:40Z"
                  },
                  "urls": {
                    "fmp4": {
                      "live": "https://cdn.tv10-poc.vustream.net/live/tv10poc/main/master.m3u8",
                      "catch_up": "https://cdn.tv10-poc.vustream.net/m/catchup/main/{start_ms}/{end_ms}/master.m3u8?c=tv10poc"
                    },
                    "ts": {
                      "live": "https://cdn.tv10-poc.vustream.net/m/ts/live/tv10poc/main/master.m3u8",
                      "catch_up": "https://cdn.tv10-poc.vustream.net/m/catchup/main/{start_ms}/{end_ms}/ts/master.m3u8?c=tv10poc"
                    }
                  },
                  "warnings": [
                    "ts_disables_ll_hls",
                    "ts_disables_multi_drm",
                    "ts_larger_segments"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 1 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — no such channel in this tenant (or it is in the trash)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error` — mode must be fmp4, ts or both",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — packaging options are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/assets/{id}/packaging": {
      "get": {
        "tags": [
          "packaging"
        ],
        "summary": "Effective output packaging of a VOD asset (override → tenant → fmp4) and its URLs",
        "description": "**Required scope:** `assets:read`\n\nEffective output packaging of a VOD asset (override → tenant → fmp4) and its URLs. Encrypted (AES-128/DRM) assets stay fMP4 only.\n\nResolves the asset's mode: its override if set, else the tenant default, else `fmp4` (`source` says which).\n`urls.fmp4.vod` is always present; `urls.ts.vod` only when the mode is `ts` or `both`. A deleted asset or one\nof another tenant is 404.",
        "operationId": "getAssetPackaging",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Effective packaging and URLs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                },
                "example": {
                  "mode": "fmp4",
                  "source": "default",
                  "tenant": {
                    "mode": null
                  },
                  "asset": {
                    "mode": null
                  },
                  "urls": {
                    "fmp4": {
                      "vod": "https://cdn.now14-poc.vustream.net/vod/now14poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/master.m3u8"
                    }
                  },
                  "warnings": []
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — no such asset in this tenant (or it is deleted)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — packaging options are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "packaging"
        ],
        "summary": "Set an asset override ({mode}; null = inherit the tenant)",
        "description": "**Required scope:** `assets:write`\n\nSets the asset's packaging override, or removes it with `{\"mode\": null}` so the asset inherits the tenant\ndefault. No re-encode: TS is remuxed on the fly from the same CMAF segments. Answers with the same body as GET\n(effective mode + URLs). Audited as `packaging.asset`.",
        "operationId": "putAssetPackaging",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 1 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PackagingRequest"
              },
              "example": {
                "mode": "both"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the effective asset packaging after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                },
                "example": {
                  "mode": "both",
                  "source": "asset",
                  "tenant": {
                    "mode": null
                  },
                  "asset": {
                    "mode": "both",
                    "updated_at": "2026-10-06T08:07:12Z"
                  },
                  "urls": {
                    "fmp4": {
                      "vod": "https://cdn.now14-poc.vustream.net/vod/now14poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/master.m3u8"
                    },
                    "ts": {
                      "vod": "https://cdn.now14-poc.vustream.net/m/ts/vod/now14poc/0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f/master.m3u8"
                    }
                  },
                  "warnings": [
                    "ts_alternate_no_ll_hls",
                    "ts_alternate_no_drm"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 1 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — no such asset in this tenant (or it is deleted)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error` — mode must be fmp4, ts or both",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — packaging options are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/lipsync/settings": {
      "get": {
        "tags": [
          "lipsync"
        ],
        "summary": "Lip-sync defaults of the tenant",
        "description": "**Required scope:** `channels:read`\n\nLip-sync defaults of the tenant: monitor = sample the recorded audio/video offset; correct = re-align the audio of recordings (never live)\n\nThe tenant row that channels inherit unless they override it (PUT /v1/channels/{id}/lipsync/settings). Fields\nthat were never set are null (monitor and correct then count as off, the cadence as 15 min). `available` is the\nplatform switch (LIPSYNC_ENABLED): when the platform has lip-sync off and not configured, this answers 200 with\nall fields null and `available: false` instead of 503. Requires scope `channels:read`; platform tenants only.",
        "operationId": "getLipsyncSettings",
        "responses": {
          "200": {
            "description": "Settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LipsyncTenantSettings"
                },
                "example": {
                  "monitor": true,
                  "correct": false,
                  "interval_min": 15,
                  "updated_at": "2026-10-03T07:20:11Z",
                  "available": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: lip-sync monitoring is not available (not configured or its migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "lipsync"
        ],
        "summary": "Set the tenant's lip-sync defaults (channels inherit them unless they override)",
        "description": "**Required scope:** `tenant:settings`\n\nReplaces the tenant row: send all three fields — an omitted or null field is stored as not set (monitor/correct\noff, cadence 15 min). `interval_min` must be 5, 15, 30 or 60. `correct` only affects recordings (catch-up,\nstart-over, clips), never the live edge. Turning monitor or correct on is refused with 409 while the platform\nhas lip-sync off. Unknown fields (also `available`) are rejected with 400; body at most 4 KiB. Requires scope\n`tenant:settings`. Audited as `lipsync.settings`.",
        "operationId": "putLipsyncSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LipsyncSettings"
              },
              "example": {
                "monitor": true,
                "correct": true,
                "interval_min": 15
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LipsyncTenantSettings"
                },
                "example": {
                  "monitor": true,
                  "correct": true,
                  "interval_min": 15,
                  "updated_at": "2026-10-06T10:02:45Z",
                  "available": true
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 4 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "`unavailable`: lip-sync is turned off on this platform (LIPSYNC_ENABLED=false) and cannot be switched on; switching off stays allowed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: interval_min must be 5, 15, 30 or 60 (or null)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: lip-sync monitoring is not available (not configured or its migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/channels/{id}/lipsync": {
      "get": {
        "tags": [
          "lipsync"
        ],
        "summary": "Lip-sync of a channel: effective settings, current status, samples and correction spans of a window",
        "description": "**Required scope:** `channels:read`\n\nReturns the channel's resolved settings (with the tenant and channel rows they come from), the status of the\nlast max(10 min, interval + 5 min), every sample in [from, to) oldest first and the correction spans that\noverlap it. The window defaults to the last 24 h and may be at most 8 days. `offset_ms` > 0 means the audio is\nearly. `available: false` (platform switch off) means the history is read-only. Requires scope\n`channels:read`; platform tenants only.",
        "operationId": "getChannelLipsync",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Window start, RFC 3339 (default to − 24 h)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Window end (exclusive), RFC 3339 (default now); after from, at most 8 days later",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lip-sync state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "settings",
                    "status",
                    "samples",
                    "spans",
                    "frame_ms",
                    "available"
                  ],
                  "properties": {
                    "settings": {
                      "$ref": "#/components/schemas/LipsyncEffective"
                    },
                    "status": {
                      "$ref": "#/components/schemas/LipsyncStatus"
                    },
                    "samples": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LipsyncSample"
                      }
                    },
                    "spans": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LipsyncSpan"
                      }
                    },
                    "frame_ms": {
                      "type": "number",
                      "description": "correction step (one AAC frame at 48 kHz, 21.333 ms)"
                    },
                    "available": {
                      "type": "boolean",
                      "description": "false when lip-sync is turned off on this platform (LIPSYNC_ENABLED=false): history read-only, no switches"
                    },
                    "compare": {
                      "$ref": "#/components/schemas/LipsyncComparison"
                    },
                    "input_sources": {
                      "type": "array",
                      "description": "v3: the channel's input legs that can be measured (HLS pull feeds of Studio-managed ingest, or the operator relay's HLS source); omitted when none",
                      "items": {
                        "type": "object",
                        "properties": {
                          "leg": {
                            "type": "string",
                            "enum": [
                              "a",
                              "b"
                            ]
                          },
                          "origin": {
                            "type": "string",
                            "enum": [
                              "managed",
                              "relay"
                            ]
                          }
                        }
                      }
                    },
                    "input_reason": {
                      "type": "string",
                      "description": "why the input cannot be measured (e.g. an SRT/RTMP feed); omitted when it can"
                    }
                  }
                },
                "example": {
                  "settings": {
                    "monitor": true,
                    "correct": true,
                    "interval_min": 15,
                    "tenant": {
                      "monitor": true,
                      "correct": false,
                      "interval_min": 15,
                      "updated_at": "2026-10-03T07:20:11Z"
                    },
                    "channel": {
                      "monitor": null,
                      "correct": true,
                      "interval_min": null,
                      "updated_at": "2026-10-04T12:00:00Z"
                    }
                  },
                  "status": {
                    "median_ms": -106.4,
                    "confident_samples": 3,
                    "last_sample_at": "2026-10-06T09:46:10Z",
                    "state": "ok",
                    "interval_min": 15
                  },
                  "samples": [
                    {
                      "at": "2026-10-06T09:30:00Z",
                      "window_ms": 10000,
                      "kind": "monitor",
                      "status": "ok",
                      "offset_ms": -106,
                      "confidence": 6.4,
                      "faces": 1,
                      "spread_ms": 12.5
                    },
                    {
                      "at": "2026-10-06T09:45:00Z",
                      "window_ms": 10000,
                      "kind": "monitor",
                      "status": "no_face",
                      "offset_ms": null,
                      "confidence": null,
                      "faces": 0
                    }
                  ],
                  "spans": [
                    {
                      "id": "01a0f1c3-2a9b-7c44-8e15-3f6d7a8b9c01",
                      "start_at": "2026-10-06T06:00:00Z",
                      "end_at": "2026-10-06T06:42:30Z",
                      "offset_ms": -197,
                      "shift_frames": -9,
                      "applied_ms": -192,
                      "source": "auto",
                      "state": "ready",
                      "confidence": 6.8,
                      "samples": 11,
                      "segments": 425,
                      "updated_at": "2026-10-06T07:05:12Z"
                    }
                  ],
                  "frame_ms": 21.333,
                  "available": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: from/to are not RFC 3339, or the window is empty or longer than 8 days",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: lip-sync monitoring is not available (not configured or its migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/lipsync/settings": {
      "put": {
        "tags": [
          "lipsync"
        ],
        "summary": "Per-channel switches for the monitor and the automatic correction, and the cadence (null = inherit the tenant)",
        "description": "**Required scope:** `channels:write`\n\nReplaces the channel's override row: each field null or omitted inherits the tenant default. Returns the\nresolved settings. Turning monitor or correct on is refused with 409 while the platform has lip-sync off.\n`interval_min` must be 5, 15, 30 or 60. Unknown fields are rejected (400); body at most 4 KiB. Requires scope\n`channels:write`. Audited as `channel.lipsync`.",
        "operationId": "putChannelLipsyncSettings",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LipsyncSettings"
              },
              "example": {
                "monitor": null,
                "correct": true,
                "interval_min": 5
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Effective settings after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LipsyncEffective"
                },
                "example": {
                  "monitor": true,
                  "correct": true,
                  "interval_min": 5,
                  "tenant": {
                    "monitor": true,
                    "correct": false,
                    "interval_min": 15,
                    "updated_at": "2026-10-03T07:20:11Z"
                  },
                  "channel": {
                    "monitor": null,
                    "correct": true,
                    "interval_min": 5,
                    "updated_at": "2026-10-06T10:04:31Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 4 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`unavailable`: lip-sync is turned off on this platform (LIPSYNC_ENABLED=false) and cannot be switched on; switching off stays allowed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: interval_min must be 5, 15, 30 or 60 (or null)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: lip-sync monitoring is not available (not configured or its migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/lipsync/measure-input": {
      "post": {
        "tags": [
          "lipsync"
        ],
        "summary": "Measure the lip-sync of the channel's input (source feed) now",
        "description": "**Required scope:** `channels:write`\n\nv3 Fetches the newest ~12 s of each measurable input leg (or only `leg`) directly from the source — the\nHLS pull feeds of Studio-managed ingest, or the operator relay's HLS source — stores the clip for the scorer and\nqueues one lip-sync sample per leg (`point: input`, `kind: manual`). The result appears in GET\n/v1/channels/{id}/lipsync within a minute or two (GPU queue). The source URL never leaves the control plane.\nSRT/RTMP feeds cannot be measured at the input yet (409 `input_not_measurable`). Independent of the channel's\n`measure_at` setting. Body optional, at most 1 KiB. Requires scope `channels:write`. Audited as\n`channel.lipsync_measure_input`.",
        "operationId": "postChannelLipsyncMeasureInput",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "leg": {
                    "type": "string",
                    "enum": [
                      "a",
                      "b"
                    ],
                    "description": "omit for every measurable leg"
                  }
                }
              },
              "example": {
                "leg": "a"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Samples queued (per leg; a leg whose capture failed carries `error` and no job)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "queued"
                  ],
                  "properties": {
                    "queued": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "leg": {
                            "type": "string",
                            "enum": [
                              "a",
                              "b"
                            ]
                          },
                          "job_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "start of the captured window on the platform clock (the recording timeline)"
                          },
                          "variant": {
                            "type": "string",
                            "description": "the source variant measured (WxH@kbps), nearest to 720p"
                          },
                          "error": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "queued": [
                    {
                      "leg": "a",
                      "job_id": "01a0f3aa-1b2c-7d3e-8f40-5a6b7c8d9e0f",
                      "at": "2026-10-08T09:41:20Z",
                      "variant": "1280x720@3000k"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON or has unknown fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`unavailable`: lip-sync is turned off on this platform; `input_not_measurable`: no HLS input leg (the detail says why)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: leg must be a or b",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: lip-sync monitoring is not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/input-monitor": {
      "get": {
        "tags": [
          "input-monitor"
        ],
        "summary": "Input monitoring of a channel: settings and the newest status of each input leg",
        "description": "**Required scope:** `channels:read`\n\nv3 Per contribution leg (a = primary, b = backup) the newest snapshot the encoder agent reported:\navailability, received vs declared bitrate, format (codec/profile/resolution/frame rate/scan), TS continuity\n(a TR 101 290 priority 1/2 subset), HLS playlist health (pull_hls), the newest sampled decode (black, frozen,\nsilence, EBU R128 short-term loudness) and the A/V PTS drift, with the evaluated conditions and since when\neach holds; plus the A/B comparison when both legs report. `available: false` = the platform switch\nINPUT_MONITOR_ENABLED is off; `managed: false` = the channel does not run Studio-managed ingest (the monitor\nruns in the encoder agent, so nothing is reported). Requires scope `channels:read`.",
        "operationId": "getChannelInputMonitor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Settings and status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "available",
                    "managed",
                    "settings",
                    "legs"
                  ],
                  "properties": {
                    "available": {
                      "type": "boolean"
                    },
                    "managed": {
                      "type": "boolean"
                    },
                    "settings": {
                      "$ref": "#/components/schemas/InputMonitorSettings"
                    },
                    "legs": {
                      "type": "object",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/InputLegStatus"
                      },
                      "description": "keyed a | b"
                    },
                    "compare": {
                      "$ref": "#/components/schemas/InputCompare"
                    }
                  }
                },
                "example": {
                  "available": true,
                  "managed": true,
                  "settings": {
                    "enabled": true,
                    "interval_s": 10,
                    "decode_every_s": 30,
                    "declared_kbps_a": null,
                    "declared_kbps_b": null,
                    "updated_at": "2026-10-08T09:00:00Z"
                  },
                  "legs": {
                    "a": {
                      "snapshot": {
                        "at": "2026-10-08T09:41:20Z",
                        "interval_s": 10,
                        "mode": "pull_hls",
                        "up": true,
                        "up_s": 10,
                        "kbps": 3184,
                        "declared_kbps": 3300,
                        "ts": {
                          "packets": 21180,
                          "bytes": 3981840,
                          "sync_loss": 0,
                          "pat_errors": 0,
                          "pmt_errors": 0,
                          "cc_errors": 0,
                          "pcr_errors": 0,
                          "pts_errors": 0,
                          "pts_jumps": 0,
                          "source_corrupt": 0,
                          "source_ts_issues": 0,
                          "video_stream_type": 27,
                          "audio_stream_type": 15
                        },
                        "hls": {
                          "variant": "1920x1080@3300k",
                          "target_duration": 6,
                          "media_sequence": 812331,
                          "gaps": 0,
                          "discontinuities": 0,
                          "long_segments": 0,
                          "seq_restarts": 0,
                          "stalled": false,
                          "stall_s": 2.1,
                          "fetch_errors": 0,
                          "pdt_drift_s": 14.2,
                          "declared_kbps": 3300,
                          "segments_seen": 2,
                          "last_segment_s": 6,
                          "playlist_entries": 6
                        },
                        "decode": {
                          "at": "2026-10-08T09:41:02Z",
                          "window_s": 5.1,
                          "black_s": 0,
                          "frozen_s": 0,
                          "silent_s": 0,
                          "lufs_s": -22.8,
                          "lufs_i": -23.4,
                          "decode_errors": 0,
                          "cpu_s": 0.42,
                          "video": {
                            "codec": "h264",
                            "profile": "High",
                            "width": 1920,
                            "height": 1080,
                            "fps": 25,
                            "interlaced": true,
                            "pix_fmt": "yuv420p"
                          },
                          "audio": {
                            "codec": "aac",
                            "profile": "LC",
                            "rate": 48000,
                            "channels": "stereo"
                          }
                        },
                        "av_delta_ms": -412,
                        "av_drift_ms": 3,
                        "restarts": 0
                      },
                      "conditions": {
                        "down": false,
                        "black": false,
                        "frozen": false,
                        "silent": false,
                        "loudness": false,
                        "continuity": false,
                        "hls_gap": false,
                        "hls_stall": false,
                        "bitrate_low": false,
                        "av_drift": false
                      },
                      "since": {},
                      "age_s": 3.2
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: input monitoring is not available (migration 0087 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/input-monitor/settings": {
      "put": {
        "tags": [
          "input-monitor"
        ],
        "summary": "Switch input monitoring of a channel on or off and set its cadence",
        "description": "**Required scope:** `channels:write`\n\nReplaces the channel's settings (off by default). Switching it on is refused with 409 while the platform switch\nINPUT_MONITOR_ENABLED is off; switching off is always allowed. The encoder agent picks the change up within a\nfew seconds without touching the feed. `declared_kbps_a|b` override the declared bitrate the InputBitrateLow\nrule compares with (default: the HLS variant's BANDWIDTH; unknown for SRT/RTMP). Body at most 4 KiB, unknown\nfields rejected. Requires scope `channels:write`. Audited as `channel.input_monitor`.",
        "operationId": "putChannelInputMonitorSettings",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InputMonitorSettings"
              },
              "example": {
                "enabled": true,
                "interval_s": 10,
                "decode_every_s": 30,
                "declared_kbps_a": null,
                "declared_kbps_b": 2500
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InputMonitorSettings"
                },
                "example": {
                  "enabled": true,
                  "interval_s": 10,
                  "decode_every_s": 30,
                  "declared_kbps_a": null,
                  "declared_kbps_b": 2500,
                  "updated_at": "2026-10-08T09:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 4 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`unavailable`: input monitoring is turned off on this platform",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: interval_s 5..60, decode_every_s 10..300, declared_kbps 50..200000 or null",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: input monitoring is not available (migration 0087 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/input-monitor/timeline": {
      "get": {
        "tags": [
          "input-monitor"
        ],
        "summary": "Input monitoring timeline: one row per leg and minute",
        "description": "**Required scope:** `channels:read`\n\nMinute rows of the last `hours` (default 24, at most 48; rows are kept 48 h): seconds up, received bitrate\n(min/avg), continuity errors, HLS media-sequence gaps, the largest black/frozen/silent share of a sampled decode,\nshort-term loudness and A/V drift. Requires scope `channels:read`.",
        "operationId": "getChannelInputMonitorTimeline",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "hours",
            "in": "query",
            "description": "1..48 (default 24)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 48
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Minute rows, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "hours": {
                      "type": "integer"
                    },
                    "minutes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "leg": {
                            "type": "string",
                            "enum": [
                              "a",
                              "b"
                            ]
                          },
                          "t": {
                            "type": "integer",
                            "description": "minute start, unix seconds"
                          },
                          "up_s": {
                            "type": "integer"
                          },
                          "samples": {
                            "type": "integer"
                          },
                          "kbps_min": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "kbps_avg": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "errors": {
                            "type": "integer"
                          },
                          "hls_gaps": {
                            "type": "integer"
                          },
                          "black": {
                            "type": "number",
                            "description": "0..1"
                          },
                          "frozen": {
                            "type": "number",
                            "description": "0..1"
                          },
                          "silent": {
                            "type": "number",
                            "description": "0..1"
                          },
                          "lufs_s": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "av_drift_ms": {
                            "type": [
                              "number",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "hours": 24,
                  "minutes": [
                    {
                      "leg": "a",
                      "t": 1791452460,
                      "up_s": 60,
                      "samples": 6,
                      "kbps_min": 3102,
                      "kbps_avg": 3190,
                      "errors": 0,
                      "hls_gaps": 0,
                      "black": 0,
                      "frozen": 0,
                      "silent": 0,
                      "lufs_s": -22.8,
                      "av_drift_ms": 3
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: hours must be 1..48",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: input monitoring is not available (migration 0087 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/input-monitor/events": {
      "get": {
        "tags": [
          "input-monitor"
        ],
        "summary": "Input monitoring events: when a condition started and ended, format changes, A/B mismatches",
        "description": "**Required scope:** `channels:read`\n\nEvents of [from, to) newest first (default the last 7 days, at most 31; events are kept 30 days). `kind`:\ndown, black, frozen, silent, loudness, continuity, hls_gap, hls_stall, bitrate_low, av_drift (state start|end;\nan end carries `duration_s`), format_change (info) and ab_mismatch (leg `ab`). Requires scope `channels:read`.",
        "operationId": "getChannelInputMonitorEvents",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1..1000 (default 200)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "leg": {
                            "type": "string",
                            "enum": [
                              "a",
                              "b",
                              "ab"
                            ]
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "down",
                              "black",
                              "frozen",
                              "silent",
                              "loudness",
                              "continuity",
                              "hls_gap",
                              "hls_stall",
                              "bitrate_low",
                              "av_drift",
                              "format_change",
                              "ab_mismatch"
                            ]
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "start",
                              "end",
                              "info"
                            ]
                          },
                          "detail": {
                            "type": "object",
                            "additionalProperties": true
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "events": [
                    {
                      "id": "01a0f3b1-77aa-7c00-9d11-223344556677",
                      "leg": "a",
                      "at": "2026-10-08T07:12:40Z",
                      "kind": "silent",
                      "state": "end",
                      "detail": {
                        "duration_s": 74,
                        "silent_s": 0,
                        "window_s": 5.1
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: from/to are not RFC 3339, or the window is empty or longer than 31 days",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: input monitoring is not available (migration 0087 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/fast/channels": {
      "get": {
        "tags": [
          "fast"
        ],
        "summary": "List the FAST channels",
        "description": "**Required scope:** `channels:read`\n\nFAST channels are linear channels assembled from library assets and approved clips by the manifest\nservice — no encoder. `available` is false while the platform switch FAST_CHANNELS_ENABLED is off (then the\nother FAST routes answer 503). Requires scope `channels:read`.",
        "operationId": "listFastChannels",
        "responses": {
          "200": {
            "description": "The tenant's FAST channels by slug",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "available": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FastChannel"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "fast"
        ],
        "summary": "Create a FAST channel",
        "description": "**Required scope:** `channels:write`\n\nCreates a channel off air (`enabled` false; EPG output and SSAI off). `kind` scheduled (weekly schedules from\nrules) or best_of (filled from approved AI clips); `ladder` defaults to the tenant's default ladder — every item\nmust have that ladder's renditions (`ladder_policy` exclude leaves other assets out of generated schedules,\nflag places them and lets the checker report them). Body at most 256 KiB. Requires scope `channels:write`.\nAudited as `fast.channel_create`.",
        "operationId": "createFastChannel",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FastChannelInput"
              },
              "example": {
                "slug": "best-of-news",
                "title": "Best of news",
                "kind": "scheduled",
                "ladder": "news-1080p"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastChannel"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: not valid JSON or unknown fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "`conflict`: the slug is taken",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: invalid fields (slug, ladder, rules, best_of, live channels)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: FAST channels are not available on this platform",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/fast/channels/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "fast"
        ],
        "summary": "Get a FAST channel with its URLs and what is on air",
        "description": "**Required scope:** `channels:read`\n\nRequires scope `channels:read`.",
        "operationId": "getFastChannel",
        "responses": {
          "200": {
            "description": "The channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastChannel"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "patch": {
        "tags": [
          "fast"
        ],
        "summary": "Change a FAST channel's settings and rules",
        "description": "**Required scope:** `channels:write`\n\nOnly the given fields change. `enabled` puts the channel on air (its HLS URL answers), `epg_enabled` lists it in\nthe tenant's XMLTV/JSON feeds as `fast-<slug>`, `ssai_enabled` stitches ads at its break markers with the ad\nsettings of `ssai_channel_id` (a live channel with the same ladder). Approving best-of rules\n(`best_of.approved`) also needs scope `channels:operate`. Requires scope `channels:write`. Audited as\n`fast.channel_update`.",
        "operationId": "patchFastChannel",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FastChannelInput"
              },
              "example": {
                "enabled": true,
                "epg_enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastChannel"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: not valid JSON or unknown fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`: the slug is taken",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: invalid fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      },
      "delete": {
        "tags": [
          "fast"
        ],
        "summary": "Delete a FAST channel",
        "description": "**Required scope:** `channels:write`\n\nDeletes the channel with its schedules and break-ins. Requires scope `channels:write`. Audited as `fast.channel_delete`.",
        "operationId": "deleteFastChannel",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/fast/channels/{id}/schedules": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "fast"
        ],
        "summary": "List a FAST channel's schedules",
        "description": "**Required scope:** `channels:read`\n\nNewest range first, without items. Requires scope `channels:read`.",
        "operationId": "listFastSchedules",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The schedules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FastSchedule"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "fast"
        ],
        "summary": "Generate a schedule from the rules, or make one from items",
        "description": "**Required scope:** `channels:write`\n\nWithout `items`: runs the rules-based scheduler for `days` (1–14, default 7) from `start` — dayparts in the\nchannel's time zone, rights windows, ratings, the repeat policy, fillers and the break pattern — and stores a\ndraft (a week of a 2,000-asset library takes well under a second; `generated_ms`). With `items`: a hand-made\ndraft, laid end to end from `start`. Every slot is a whole number of segments, so a schedule has no gaps.\nRequires scope `channels:write`. Audited as `fast.schedule_generate` / `fast.schedule_create`.",
        "operationId": "createFastSchedule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "start"
                ],
                "properties": {
                  "start": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 14
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/FastEditItem"
                    }
                  }
                }
              },
              "example": {
                "start": "2026-10-11T00:00:00+03:00",
                "days": 7
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The draft schedule with its items, and the scheduler's warnings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schedule": {
                      "$ref": "#/components/schemas/FastSchedule"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: not valid JSON or unknown fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: no rules, nothing eligible to play, or invalid items",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/fast/channels/{id}/schedules/{sid}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "sid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "fast"
        ],
        "summary": "Get a schedule with its items",
        "description": "**Required scope:** `channels:read`\n\nRequires scope `channels:read`.",
        "operationId": "getFastSchedule",
        "responses": {
          "200": {
            "description": "The schedule",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastSchedule"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel or schedule",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "delete": {
        "tags": [
          "fast"
        ],
        "summary": "Delete an unpublished schedule",
        "description": "**Required scope:** `channels:write`\n\nRequires scope `channels:write`.",
        "operationId": "deleteFastSchedule",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel or schedule",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`: the schedule is published",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/fast/channels/{id}/schedules/{sid}/items": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "sid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "put": {
        "tags": [
          "fast"
        ],
        "summary": "Replace a schedule's items (reorder, add, remove)",
        "description": "**Required scope:** `channels:write`\n\nThe items are laid end to end from the schedule's start (no gaps); the schedule goes back to draft and must be\nchecked again. Published schedules cannot be edited. Body at most 8 MiB. Requires scope `channels:write`.",
        "operationId": "putFastScheduleItems",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "items"
                ],
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/FastEditItem"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The schedule",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastSchedule"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: not valid JSON or unknown fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel or schedule",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`: the schedule is published",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: invalid items",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/fast/channels/{id}/schedules/{sid}/check": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "sid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "fast"
        ],
        "summary": "Run the playlist checker on a schedule",
        "description": "**Required scope:** `channels:write`\n\nChecks gaps and overlaps, missing or unready media, missing renditions and ladder/codec mismatches, encrypted\nassets, rights windows (`metadata.rights.start|end` of an asset), parental ratings (`metadata.rating` against\nthe block's `max_rating`), unapproved AI clips and the repeat policy; rights and rating errors suggest a\nreplacement. `deep` also reads the stored playlists. Errors make the schedule `blocked` and alert the tenant\n(notification event `fast.check_failed`, email / Telegram rules). Requires scope `channels:write`.",
        "operationId": "checkFastSchedule",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deep": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The verdict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastCheck"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel or schedule",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/fast/channels/{id}/schedules/{sid}/publish": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "sid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "fast"
        ],
        "summary": "Check and publish a schedule",
        "description": "**Required scope:** `channels:operate`\n\nRuns the deep check; any error blocks publishing (409 `blocked` with the verdict, the tenant is alerted).\nOtherwise the schedule goes on air from its first item that starts after what airs now — nothing on air is cut\nand the past is never rewritten — replacing the overlapping items of earlier schedules. Requires scope\n`channels:operate`. Audited as `fast.schedule_publish` (or `fast.schedule_blocked`).",
        "operationId": "publishFastSchedule",
        "responses": {
          "200": {
            "description": "Published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schedule": {
                      "$ref": "#/components/schemas/FastSchedule"
                    },
                    "on_air_from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "check": {
                      "$ref": "#/components/schemas/FastCheck"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel or schedule",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`blocked` by the playlist checker (body has `check`), or `conflict`: already published / its range has passed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    },
                    "check": {
                      "$ref": "#/components/schemas/FastCheck"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:operate"
      }
    },
    "/v1/fast/channels/{id}/best-of/preview": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "fast"
        ],
        "summary": "Preview the clips a best-of channel's rules select",
        "description": "**Required scope:** `channels:read`\n\nApproved (or published) AI clip artefacts of the last `best_of.hours` that are linked to a playable clip of a\nchannel with the same ladder, filtered by topics and length, in play order. Rejected or unapproved clips never\nappear. Requires scope `channels:read`.",
        "operationId": "previewFastBestOf",
        "responses": {
          "200": {
            "description": "The clips",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FastClipCandidate"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/fast/channels/{id}/best-of/refresh": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "fast"
        ],
        "summary": "Programme and publish a best-of channel now",
        "description": "**Required scope:** `channels:operate`\n\nSchedules the selected clips (looped) from the end of what airs now for `best_of.horizon_min` minutes, checks\nand publishes it. Once the rules are approved the orchestrator does this by itself every `best_of.refresh_min`\nminutes. Requires scope `channels:operate`. Audited as `fast.best_of_refresh`.",
        "operationId": "refreshFastBestOf",
        "responses": {
          "200": {
            "description": "Published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schedule": {
                      "$ref": "#/components/schemas/FastSchedule"
                    },
                    "check": {
                      "$ref": "#/components/schemas/FastCheck"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`blocked` by the playlist checker (body has `check`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: not a best-of channel, or no approved clip matches",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:operate"
      }
    },
    "/v1/fast/channels/{id}/break-ins": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "fast"
        ],
        "summary": "List a FAST channel's live break-ins",
        "description": "**Required scope:** `channels:read`\n\nThe last 7 days and the scheduled ones, newest first. Requires scope `channels:read`.",
        "operationId": "listFastBreakIns",
        "responses": {
          "200": {
            "description": "The break-ins",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FastBreakIn"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "fast"
        ],
        "summary": "Switch a FAST channel to a live channel",
        "description": "**Required scope:** `channels:operate`\n\nFrom `start_at` (default now) the channel plays the live channel's recording (same ladder required); viewers\nmove within one segment. It returns to the schedule at the first item that starts after `end_at`, or after the\nbreak-in is ended. Requires scope `channels:operate`. Audited as `fast.break_in`.",
        "operationId": "createFastBreakIn",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "live_channel_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "default: the channel's live_channel_id"
                  },
                  "start_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "end_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              },
              "example": {
                "live_channel_id": "01a0f3b1-77aa-7c00-9d11-223344556677"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The break-in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastBreakIn"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: not valid JSON or unknown fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`: another break-in overlaps",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: no such live channel, another ladder, or end before start",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:operate"
      }
    },
    "/v1/fast/channels/{id}/break-ins/{bid}/end": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "bid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "fast"
        ],
        "summary": "End a live break-in",
        "description": "**Required scope:** `channels:operate`\n\nA running break-in ends now and the channel returns to the schedule at the next slot; one that has not started\nis cancelled. Requires scope `channels:operate`. Audited as `fast.break_in_end`.",
        "operationId": "endFastBreakIn",
        "responses": {
          "200": {
            "description": "The break-in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastBreakIn"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such FAST channel or break-in",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`: not running or scheduled",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:operate"
      }
    },
    "/v1/channels/{id}/lipsync/spans": {
      "post": {
        "tags": [
          "lipsync"
        ],
        "summary": "Add a manual correction span to a channel's recording",
        "description": "**Required scope:** `channels:write`\n\nCreates a manual span (state `pending`) that shifts the recorded audio of [start_at, end_at) by `offset_ms`,\nrounded to whole AAC frames (21.333 ms); the correction worker then renders it (state `ready`, or `failed`).\nIt affects recordings only (catch-up, start-over, clips), never live. The span may be at most 6 h long and\n|offset_ms| must be between half a frame (~10.7 ms; the error message says 11) and 800 ms. Always refused with\n409 while the platform has lip-sync off. Body at most 4 KiB, unknown fields rejected. Requires scope\n`channels:write`. Audited as `channel.lipsync_span`.",
        "operationId": "postChannelLipsyncSpan",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "start_at",
                  "end_at",
                  "offset_ms"
                ],
                "properties": {
                  "start_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "end_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "after start_at, at most 6 h later"
                  },
                  "offset_ms": {
                    "type": "number",
                    "minimum": -800,
                    "maximum": 800,
                    "description": "> 0 = audio early; |offset_ms| ≥ 10.67 (half a frame)"
                  }
                }
              },
              "example": {
                "start_at": "2026-10-06T06:00:00Z",
                "end_at": "2026-10-06T06:45:00Z",
                "offset_ms": -180
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created; read it back with GET /v1/channels/{id}/lipsync",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                },
                "example": {
                  "id": "01a0f2d9-4e6b-7a10-9c2d-8e7f6a5b4c3d"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 4 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`unavailable`: lip-sync is turned off on this platform; no manual spans",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: start_at < end_at (≤ 6 h) and a valid offset_ms are required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: lip-sync monitoring is not available (not configured or its migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/lipsync/spans/{sid}": {
      "patch": {
        "tags": [
          "lipsync"
        ],
        "summary": "Override a span: a new offset (a manual span replaces it) or turn it off",
        "description": "**Required scope:** `channels:write`\n\n`{\"disabled\": true}` turns the span off (state `disabled`; the original audio plays) and returns it.\n`{\"offset_ms\": v}` disables the span and creates a manual span over the same time range with that offset\n(rounded to whole AAC frames, state `pending`) and returns the new span — note its new `id`. With\n`disabled: true` any offset is ignored. |offset_ms| must be between half a frame (~10.7 ms) and 800 ms. Works\nfor automatic and manual spans. Body at most 4 KiB, unknown fields rejected. Requires scope `channels:write`.\nAudited as `channel.lipsync_span`.",
        "operationId": "patchChannelLipsyncSpan",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a trashed channel or an id that is not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "Span id (not a UUID answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "offset_ms": {
                    "type": "number",
                    "minimum": -800,
                    "maximum": 800,
                    "description": "the new offset (> 0 = audio early); required unless disabled is true"
                  },
                  "disabled": {
                    "type": "boolean",
                    "default": false
                  }
                }
              },
              "example": {
                "offset_ms": -150
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The resulting span (the new manual span, or the disabled one)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LipsyncSpan"
                },
                "example": {
                  "id": "01a0f2e0-1b2c-7d3e-8f40-5a6b7c8d9e0f",
                  "start_at": "2026-10-06T06:00:00Z",
                  "end_at": "2026-10-06T06:42:30Z",
                  "offset_ms": -150,
                  "shift_frames": -7,
                  "applied_ms": -149.333,
                  "source": "manual",
                  "state": "pending",
                  "samples": 0,
                  "segments": 0,
                  "updated_at": "2026-10-06T10:07:55Z"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 4 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such channel, or no such span on it",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: offset_ms (half a frame ≤ |v| ≤ 800) or disabled:true required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: lip-sync monitoring is not available (not configured or its migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/posters/{type}/{id}": {
      "get": {
        "tags": [
          "posters"
        ],
        "summary": "Smart poster of a programme/series/asset/clip",
        "description": "**Required scope:** `assets:read`\n\nSmart poster of a programme/series/asset/clip: the automatic best still (faces with eyes open and mouth closed, sharp, no graphics), up to 12 scored candidates, and the editor's choice. Image chain: editor > show/EPG artwork > automatic > a frame\n\nThe poster state of one entity: the automatic choice and its score, the scored candidates (best first; up to\n12 regular plus \"suggest more\" extras), the editor's choice and which one is `current`. An entity that was never\nscored answers `mode: none` with no candidates. A series has no stills of its own: when it has no candidates,\nthe top 3 stills of each of its 4 latest scored broadcasts are offered. Image URLs are on the tenant's CDN\nhostname. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:read`.",
        "operationId": "getPoster",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "description": "Kind of entity (an unknown type answers 404)",
            "schema": {
              "type": "string",
              "enum": [
                "programme",
                "series",
                "asset",
                "clip"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme, series, asset or clip id of this tenant (a trashed asset answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Poster state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PosterState"
                },
                "example": {
                  "entity_type": "programme",
                  "entity_id": "0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55",
                  "mode": "auto",
                  "current": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "score": 0.9002,
                    "faces": 1,
                    "eyes_open": 1,
                    "mouth_ok": 1
                  },
                  "auto": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "score": 0.9002,
                    "faces": 1,
                    "eyes_open": 1,
                    "mouth_ok": 1
                  },
                  "editor": null,
                  "candidates": [
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "score": 0.9002,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    },
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "score": 0.8041,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    }
                  ],
                  "scored_at": "2026-10-06T07:05:12Z",
                  "suggest": {
                    "status": "none"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such entity in this tenant, an unknown `type`, or an id that is not a UUID (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Posters are not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "posters"
        ],
        "summary": "Choose the poster (a candidate or an uploaded image of this tenant)",
        "description": "**Required scope:** `assets:write`\n\nChoose the poster (a candidate or an uploaded image of this tenant); the choice is locked against automatic changes\n\nSets the editor's choice to `path` (a candidate still or an image already in the tenant's VOD storage); the\nautomatic selection never overrides it until `DELETE` resets it. For a series, asset or clip a recorder still\n(`/rec/…`) is first copied to `/vod/<tenant>/posters/…`, because recordings expire with the channel's\nretention (422 when the still is gone). Repeating the call with the same path is harmless. Audited as\n`poster.choose`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "putPoster",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "description": "Kind of entity (an unknown type answers 404)",
            "schema": {
              "type": "string",
              "enum": [
                "programme",
                "series",
                "asset",
                "clip"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme, series, asset or clip id of this tenant (a trashed asset answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "path"
                ],
                "additionalProperties": false,
                "properties": {
                  "path": {
                    "type": "string",
                    "maxLength": 512,
                    "description": "/rec/<tenant prefix>/… or /vod/<tenant prefix>/…, ending.jpg,.jpeg or.png; no `..`, `?`, `#`, `\\` or spaces"
                  }
                }
              },
              "example": {
                "path": "/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Poster state with the editor's choice",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PosterState"
                },
                "example": {
                  "entity_type": "programme",
                  "entity_id": "0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55",
                  "mode": "editor",
                  "current": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg"
                  },
                  "auto": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "score": 0.9002,
                    "faces": 1,
                    "eyes_open": 1,
                    "mouth_ok": 1
                  },
                  "editor": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg"
                  },
                  "editor_by": "key:k7Qm2xPa",
                  "editor_at": "2026-10-06T08:11:40Z",
                  "candidates": [
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "score": 0.9002,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    },
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "score": 0.8041,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    }
                  ],
                  "scored_at": "2026-10-06T07:05:12Z",
                  "suggest": {
                    "status": "none"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 4 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such entity in this tenant, an unknown `type`, or an id that is not a UUID (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`path` is not a JPEG/PNG path in this tenant's recordings or VOD storage, or the recorder still no longer exists (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "Copying the recorder still to VOD storage failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Posters (or, for copying a still, object storage) are not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      },
      "delete": {
        "tags": [
          "posters"
        ],
        "summary": "Reset to automatic (clears the editor's choice)",
        "description": "**Required scope:** `assets:write`\n\nClears the editor's choice; `current` falls back to the automatic choice (or `none`). Safe to repeat; an\nentity without a poster row answers its (empty) state. Uploaded images stay in storage. Audited as\n`poster.reset`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "deletePoster",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "description": "Kind of entity (an unknown type answers 404)",
            "schema": {
              "type": "string",
              "enum": [
                "programme",
                "series",
                "asset",
                "clip"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme, series, asset or clip id of this tenant (a trashed asset answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Poster state after the reset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PosterState"
                },
                "example": {
                  "entity_type": "programme",
                  "entity_id": "0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55",
                  "mode": "auto",
                  "current": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "score": 0.9002,
                    "faces": 1,
                    "eyes_open": 1,
                    "mouth_ok": 1
                  },
                  "auto": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "score": 0.9002,
                    "faces": 1,
                    "eyes_open": 1,
                    "mouth_ok": 1
                  },
                  "editor": null,
                  "candidates": [
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "score": 0.9002,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    },
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "score": 0.8041,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    }
                  ],
                  "scored_at": "2026-10-06T07:05:12Z",
                  "suggest": {
                    "status": "none"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such entity in this tenant, an unknown `type`, or an id that is not a UUID (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Posters are not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/images/settings": {
      "get": {
        "tags": [
          "images"
        ],
        "summary": "AI image settings of the tenant",
        "description": "**Required scope:** `assets:read`\n\nAI image settings of the tenant: auto_upscale = enlarge small posters, imported artwork and stills automatically (off by default)\n\nWhether the automatic sweep enlarges the tenant's small images (current posters, EPG artwork, generated asset\nposters, uploaded series/asset artwork) every ~2 minutes at the lowest priority. `auto_upscale` is false and\n`updated_*` are omitted when the tenant never set it. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:read`.",
        "operationId": "getImageSettings",
        "responses": {
          "200": {
            "description": "Settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageSettings"
                },
                "example": {
                  "auto_upscale": true,
                  "updated_at": "2026-10-02T12:30:00Z",
                  "updated_by": "key:k7Qm2xPa"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI image upscaling is not available on this deployment (`feature_disabled`; migration not applied or jobs cannot be queued)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "images"
        ],
        "summary": "Turn automatic AI upscaling on or off for the tenant",
        "description": "**Required scope:** `tenant:settings`\n\nStores `auto_upscale` (an omitted value means false). Turning it off stops new automatic upscales; existing\nvariants keep being served. `updated_at`/`updated_by` in the body are ignored. Audited as `images.settings`.\nNot available to CDN-only tenants (403 `feature_disabled`). Scope `tenant:settings`.",
        "operationId": "putImageSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImageSettings"
              },
              "example": {
                "auto_upscale": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageSettings"
                },
                "example": {
                  "auto_upscale": true,
                  "updated_at": "2026-10-06T10:00:00Z",
                  "updated_by": "key:k7Qm2xPa"
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 4 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI image upscaling is not available on this deployment (`feature_disabled`; migration not applied or jobs cannot be queued)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/tenant/branding": {
      "get": {
        "tags": [
          "settings"
        ],
        "summary": "Tenant branding (Studio → Settings → Branding)",
        "description": "**Required scope:** `assets:read`\n\nTenant branding (Studio → Settings → Branding): display name, logos, colours, favicon, e-mail sender name, with the effective colours and the contrast report\n\nReturns the stored branding (every field is a string, `\"\"` = not set) plus what the platform derives from it:\n`effective.name` (the display name, else the tenant name), the text colour (`*_ink`) for each brand colour\n(the accent falls back to the primary colour), and the WCAG contrast report. Always 200: a tenant without\nbranding, or a platform without the branding table, gets empty fields. Available to CDN-only tenants too.",
        "operationId": "getTenantBranding",
        "responses": {
          "200": {
            "description": "Branding",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantBranding"
                },
                "example": {
                  "display_name": "Channel 14",
                  "logo_light": "https://cdn.now14-poc.vustream.net/img/now14poc/brand/logo-light.png",
                  "logo_dark": "https://cdn.now14-poc.vustream.net/img/now14poc/brand/logo-dark.png",
                  "primary_color": "#1d4ed8",
                  "accent_color": "#f59e0b",
                  "favicon": "",
                  "email_from_name": "Channel 14 Studio",
                  "updated_at": "2026-10-01T09:12:44Z",
                  "updated_by": "key:Q4mT9xLp",
                  "effective": {
                    "name": "Channel 14",
                    "primary_color": "#1d4ed8",
                    "primary_ink": "#ffffff",
                    "accent_color": "#f59e0b",
                    "accent_ink": "#0b1220"
                  },
                  "contrast": [
                    {
                      "pair": "text / primary",
                      "ratio": 6.7,
                      "min": 4.5,
                      "ok": true,
                      "required": true
                    },
                    {
                      "pair": "primary / light surface",
                      "ratio": 6.7,
                      "min": 3,
                      "ok": true,
                      "required": false
                    },
                    {
                      "pair": "primary / dark surface",
                      "ratio": 2.58,
                      "min": 3,
                      "ok": false,
                      "required": false
                    },
                    {
                      "pair": "text / accent",
                      "ratio": 8.72,
                      "min": 4.5,
                      "ok": true,
                      "required": true
                    },
                    {
                      "pair": "accent / light surface",
                      "ratio": 2.15,
                      "min": 3,
                      "ok": false,
                      "required": false
                    },
                    {
                      "pair": "accent / dark surface",
                      "ratio": 8.05,
                      "min": 3,
                      "ok": true,
                      "required": false
                    }
                  ],
                  "contrast_ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "settings"
        ],
        "summary": "Save the tenant branding",
        "description": "**Required scope:** `tenant:settings`\n\nSave the tenant branding. Colours must reach WCAG AA (4.5:1) with their text colour; logos and favicon are https URLs of images uploaded to ViewStream. Used by e-mails, the watch/embed pages and as the start of NEW player configs and Sites themes (existing ones are not changed).\n\nReplaces the whole branding: a field that is omitted or `\"\"` is cleared. Text fields lose control characters\nand `< > \"` and are trimmed; colours are trimmed and lower-cased. Logo and favicon URLs must be https, at most\n1024 characters, on the tenant CDN hostname, `*.vustream.net` or `*.viewstream.co.il` (no third-party hosts).\nEach brand colour must reach 4.5:1 with its text colour (white or #0b1220, whichever is better) — otherwise\n422 naming the colour; the light/dark surface rows are advisory. Answers with the same body as GET. Audited as\n`tenant.branding`. Available to CDN-only tenants too.",
        "operationId": "putTenantBranding",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 16 KiB; unknown fields are rejected (400). `updated_at`/`updated_by` are ignored.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantBrandingInput"
              },
              "example": {
                "display_name": "Channel 14",
                "logo_light": "https://cdn.now14-poc.vustream.net/img/now14poc/brand/logo-light.png",
                "logo_dark": "https://cdn.now14-poc.vustream.net/img/now14poc/brand/logo-dark.png",
                "primary_color": "#1D4ED8",
                "accent_color": "#f59e0b",
                "favicon": "",
                "email_from_name": "Channel 14 Studio"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the branding as GET returns it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantBranding"
                },
                "example": {
                  "display_name": "Channel 14",
                  "logo_light": "https://cdn.now14-poc.vustream.net/img/now14poc/brand/logo-light.png",
                  "logo_dark": "https://cdn.now14-poc.vustream.net/img/now14poc/brand/logo-dark.png",
                  "primary_color": "#1d4ed8",
                  "accent_color": "#f59e0b",
                  "favicon": "",
                  "email_from_name": "Channel 14 Studio",
                  "updated_at": "2026-10-06T08:20:03Z",
                  "updated_by": "key:Q4mT9xLp",
                  "effective": {
                    "name": "Channel 14",
                    "primary_color": "#1d4ed8",
                    "primary_ink": "#ffffff",
                    "accent_color": "#f59e0b",
                    "accent_ink": "#0b1220"
                  },
                  "contrast": [
                    {
                      "pair": "text / primary",
                      "ratio": 6.7,
                      "min": 4.5,
                      "ok": true,
                      "required": true
                    },
                    {
                      "pair": "primary / light surface",
                      "ratio": 6.7,
                      "min": 3,
                      "ok": true,
                      "required": false
                    },
                    {
                      "pair": "primary / dark surface",
                      "ratio": 2.58,
                      "min": 3,
                      "ok": false,
                      "required": false
                    },
                    {
                      "pair": "text / accent",
                      "ratio": 8.72,
                      "min": 4.5,
                      "ok": true,
                      "required": true
                    },
                    {
                      "pair": "accent / light surface",
                      "ratio": 2.15,
                      "min": 3,
                      "ok": false,
                      "required": false
                    },
                    {
                      "pair": "accent / dark surface",
                      "ratio": 8.05,
                      "min": 3,
                      "ok": true,
                      "required": false
                    }
                  ],
                  "contrast_ok": true
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 16 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `invalid branding` (field limits, colour format, URL rules) or `the brand colours are below WCAG AA contrast`; `errors[]` names the fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "the brand colours are below WCAG AA contrast",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "errors": [
                    {
                      "field": "accent_color",
                      "detail": "contrast text / accent is 2.93:1, below WCAG AA 4.5:1"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — tenant settings are not available yet (migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/tenant/defaults": {
      "get": {
        "tags": [
          "settings"
        ],
        "summary": "Every tenant-wide default in one view (player preset, packaging, subtitles, recording retention and DVR window of new channels, time zone, Studio language, protection policy, publish, AI image upscale) with how many channels/assets choose differently",
        "description": "**Required scope:** `assets:read`\n\nRead-only aggregate of Studio → Settings → Defaults. Each value is the effective one; the `stored_*` fields\n(and `packaging.stored`) are what the tenant set — null means the platform default applies (retention 7 days,\nDVR window 14400 s, Asia/Jerusalem, `he`, fmp4). `subtitles` and `image_auto_upscale` are null when those\nfeatures are not available; `ready` is false before the tenant-settings migration (PUT then answers 503).\n`overrides` counts the channels and assets (trashed ones excluded) that differ from each default. Platform\ntenants only.",
        "operationId": "getTenantDefaults",
        "responses": {
          "200": {
            "description": "Defaults",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantDefaults"
                },
                "example": {
                  "player": {
                    "config_id": "0192b1d0-7c2e-7f3a-9b4c-5d6e7f8a9b0c",
                    "name": "Default",
                    "options": [
                      {
                        "id": "0192b1d0-7c2e-7f3a-9b4c-5d6e7f8a9b0c",
                        "name": "Default"
                      },
                      {
                        "id": "0192e4a8-11b2-7c3d-8e4f-6a7b8c9d0e1f",
                        "name": "Article embed"
                      }
                    ]
                  },
                  "packaging": {
                    "mode": "fmp4",
                    "stored": null
                  },
                  "subtitles": {
                    "enabled": true,
                    "catchup": true,
                    "vod": true,
                    "translate": [
                      "en",
                      "ar"
                    ]
                  },
                  "recording": {
                    "retention_days": 14,
                    "dvr_window_s": 14400,
                    "stored_retention_days": 14,
                    "stored_dvr_window_s": null,
                    "platform_retention_days": 7,
                    "platform_dvr_window_s": 14400
                  },
                  "timezone": "Asia/Jerusalem",
                  "stored_timezone": null,
                  "locale": "he",
                  "stored_locale": null,
                  "protection": {
                    "policy_id": null,
                    "name": ""
                  },
                  "auto_publish": true,
                  "image_auto_upscale": false,
                  "overrides": {
                    "player_channels": 0,
                    "player_assets": 3,
                    "player_sections": 1,
                    "packaging_channels": 1,
                    "packaging_assets": 0,
                    "retention_channels": 1,
                    "dvr_channels": 0,
                    "policy_channels": 0,
                    "policy_assets": 0,
                    "manual_publish_assets": 12,
                    "subtitle_optout_programmes": 4,
                    "subtitle_optout_assets": 0,
                    "subtitle_optout_clips": 0,
                    "channels": 2,
                    "assets": 1648
                  },
                  "updated_at": "2026-09-29T14:03:51Z",
                  "updated_by": "key:Q4mT9xLp",
                  "ready": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "settings"
        ],
        "summary": "Change tenant defaults",
        "description": "**Required scope:** `tenant:settings`\n\nChange tenant defaults; only the keys sent change (null = back to the platform default). Retention and DVR window apply to NEW channels only; publish applies to videos that become ready from now on. Changing the default player preset also needs delivery:write.\n\nPartial update: only the keys present change. `null` resets `retention_days`, `dvr_window_s`, `timezone`,\n`locale` and `packaging` to the platform default; `auto_publish` and `player_config_id` cannot be null (422).\n`packaging` sets the same tenant default as `PUT /v1/packaging/settings`. `player_config_id` makes that player\nconfig the tenant default; when it is not already the default the caller also needs `delivery:write` (403\n`insufficient_scope`). All fields are validated before anything is saved. Answers with the full defaults\nview (as GET). Audited as `tenant.defaults`. Platform tenants only.",
        "operationId": "putTenantDefaults",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 8 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantDefaultsInput"
              },
              "example": {
                "retention_days": 14,
                "dvr_window_s": null,
                "timezone": "Asia/Jerusalem",
                "auto_publish": false,
                "packaging": "both"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the full defaults view after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantDefaults"
                },
                "example": {
                  "player": {
                    "config_id": "0192b1d0-7c2e-7f3a-9b4c-5d6e7f8a9b0c",
                    "name": "Default",
                    "options": [
                      {
                        "id": "0192b1d0-7c2e-7f3a-9b4c-5d6e7f8a9b0c",
                        "name": "Default"
                      }
                    ]
                  },
                  "packaging": {
                    "mode": "both",
                    "stored": "both"
                  },
                  "subtitles": {
                    "enabled": true,
                    "catchup": true,
                    "vod": true,
                    "translate": [
                      "en"
                    ]
                  },
                  "recording": {
                    "retention_days": 14,
                    "dvr_window_s": 14400,
                    "stored_retention_days": 14,
                    "stored_dvr_window_s": null,
                    "platform_retention_days": 7,
                    "platform_dvr_window_s": 14400
                  },
                  "timezone": "Asia/Jerusalem",
                  "stored_timezone": "Asia/Jerusalem",
                  "locale": "he",
                  "stored_locale": null,
                  "protection": {
                    "policy_id": null,
                    "name": ""
                  },
                  "auto_publish": false,
                  "image_auto_upscale": false,
                  "overrides": {
                    "player_channels": 0,
                    "player_assets": 0,
                    "player_sections": 0,
                    "packaging_channels": 0,
                    "packaging_assets": 0,
                    "retention_channels": 1,
                    "dvr_channels": 0,
                    "policy_channels": 0,
                    "policy_assets": 0,
                    "manual_publish_assets": 0,
                    "subtitle_optout_programmes": 0,
                    "subtitle_optout_assets": 0,
                    "subtitle_optout_clips": 0,
                    "channels": 1,
                    "assets": 412
                  },
                  "updated_at": "2026-10-06T08:31:17Z",
                  "updated_by": "key:Q4mT9xLp",
                  "ready": true
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or wrong types, or is larger than 8 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope, role or tenant mode forbids the route; also `insufficient_scope` when `player_config_id` changes the default preset without `delivery:write`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error` — `invalid defaults`; `errors[]` names each field (retention_days 1..90, dvr_window_s 60..86400, timezone an IANA name, locale he|en, packaging fmp4|ts|both, auto_publish true|false, player_config_id a player config of this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "invalid defaults",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "errors": [
                    {
                      "field": "retention_days",
                      "detail": "1..90"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — tenant settings are not available yet, or `packaging` was sent while packaging options are not configured",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/tenant/catchup-exclusions": {
      "get": {
        "tags": [
          "settings"
        ],
        "summary": "Catch-up exclusions: the tenant's rules and settings",
        "description": "**Required scope:** `channels:read`\n\nCatch-up exclusions: the tenant's rules and settings. A programme whose EPG title matches an enabled rule (exact, prefix or contains on whole words, after normalising whitespace, niqqud, final letters, quotes/geresh and dashes), optionally on one channel, is excluded: not in catch-up, Sites or the catch-up APIs, no start-over, no derived jobs (subtitles, translation, boundaries, posters, previews, lip-sync), no VOD publish, and its recording is deleted early by retention after it ends. Live is never touched. `suggestion` is the Shabbat placeholder title.\n\nReturns the settings (`grace_hours`, 24 when never set — then `updated_*` are null), every rule of the tenant\noldest first with how many programmes each currently excludes, the seed `suggestion` Studio offers, and\n`tail_margin_s` (900): how long after its EPG end an excluded programme's recording may be deleted. Platform\ntenants only.",
        "operationId": "getCatchupExclusions",
        "responses": {
          "200": {
            "description": "Settings and rules",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatchupExclusions"
                },
                "example": {
                  "settings": {
                    "grace_hours": 24,
                    "updated_by": null,
                    "updated_at": null
                  },
                  "rules": [
                    {
                      "id": "0199b3e2-6a1c-7d2e-9f30-4b5c6d7e8f90",
                      "channel_id": null,
                      "channel_slug": null,
                      "match": "exact",
                      "pattern": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "normalized": "שבת שלומ שידורינו יתחדשו בצאת השבת",
                      "note": "Shabbat placeholder",
                      "enabled": true,
                      "created_by": "key:Q4mT9xLp",
                      "created_at": "2026-09-25T10:02:11Z",
                      "updated_at": "2026-09-25T10:02:11Z",
                      "excluded": 21
                    }
                  ],
                  "suggestion": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                  "tail_margin_s": 900
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — catch-up exclusions are not available yet (migration 0058 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/tenant/catchup-exclusions/settings": {
      "put": {
        "tags": [
          "settings"
        ],
        "summary": "Grace period (hours, 0–168, default 24) a programme excluded AFTER it started keeps its recording, so the…",
        "description": "**Required scope:** `tenant:settings`\n\nGrace period (hours, 0–168, default 24) a programme excluded AFTER it started keeps its recording, so the exclusion can be undone; a programme excluded before it aired is deleted 15 min after it ends\n\nStores `grace_hours` and re-resolves the tenant's excluded set at once, so the `delete_after` of programmes\nalready excluded follows the new value. Answers with the stored settings. Audited as\n`catchup.exclusions.settings` (with the previous value). Platform tenants only.",
        "operationId": "putCatchupExclusionSettings",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 4 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "grace_hours"
                ],
                "additionalProperties": false,
                "properties": {
                  "grace_hours": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 168
                  }
                }
              },
              "example": {
                "grace_hours": 48
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatchupExclusionSettings"
                },
                "example": {
                  "grace_hours": 48,
                  "updated_by": "key:Q4mT9xLp",
                  "updated_at": "2026-10-06T08:40:12Z"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 4 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `grace_hours` missing or outside 0..168",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — catch-up exclusions are not available yet (migration 0058 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/tenant/catchup-exclusions/rules": {
      "post": {
        "tags": [
          "settings"
        ],
        "summary": "Create an exclusion rule",
        "description": "**Required scope:** `tenant:settings`\n\nCreate an exclusion rule; matching programmes (aired and upcoming) leave catch-up at once. Audited (catchup.exclusion_rule.create).\n\nDefaults: `match` exact, `enabled` true, `channel_id` null (every channel). `pattern` is trimmed and stored as\nentered; `normalized` shows how it is compared; an empty `note` is stored as null. The tenant's excluded set is\nresolved before the response, so `excluded` already counts the matched programmes; recordings of aired ones\nare then deleted by retention (after `grace_hours` when they had already started). Use\n`POST /v1/tenant/catchup-exclusions/preview` first to see what a rule would match. Platform tenants only.",
        "operationId": "createCatchupExclusionRule",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 8 KiB; unknown fields are rejected (400). `pattern` is required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CatchupExclusionRuleInput"
              },
              "example": {
                "match": "exact",
                "pattern": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                "channel_id": null,
                "note": "Shabbat placeholder"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created, with the programmes it already excludes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatchupExclusionRule"
                },
                "example": {
                  "id": "0199b3e2-6a1c-7d2e-9f30-4b5c6d7e8f90",
                  "channel_id": null,
                  "channel_slug": null,
                  "match": "exact",
                  "pattern": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                  "normalized": "שבת שלומ שידורינו יתחדשו בצאת השבת",
                  "note": "Shabbat placeholder",
                  "enabled": true,
                  "created_by": "key:Q4mT9xLp",
                  "created_at": "2026-10-06T08:41:30Z",
                  "updated_at": "2026-10-06T08:41:30Z",
                  "excluded": 21
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 8 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `invalid rule`: match not exact|prefix|contains, pattern empty / over 200 characters / without letters or digits, a prefix|contains pattern under 3 letters after normalising, note over 500 characters, or channel_id not a (non-trashed) channel of this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "invalid rule",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "errors": [
                    {
                      "field": "pattern",
                      "detail": "prefix/contains patterns need at least 3 letters (too broad)"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — catch-up exclusions are not available yet (migration 0058 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/tenant/catchup-exclusions/rules/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "patch": {
        "tags": [
          "settings"
        ],
        "summary": "Change a rule (only the keys sent; channel_id null = every channel)",
        "description": "**Required scope:** `tenant:settings`\n\nChange a rule (only the keys sent; channel_id null = every channel). Programmes it no longer matches come back unless their recording was already deleted. Audited.\n\nMerges the keys sent onto the rule (`note` null or blank clears it), validates the result like create, and\nre-resolves the tenant's excluded set before answering. Disabling a rule (`enabled: false`) keeps it but\nexcludes nothing. Audited as `catchup.exclusion_rule.update` with the before/after values. Platform tenants\nonly.",
        "operationId": "patchCatchupExclusionRule",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 8 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CatchupExclusionRuleInput"
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatchupExclusionRule"
                },
                "example": {
                  "id": "0199b3e2-6a1c-7d2e-9f30-4b5c6d7e8f90",
                  "channel_id": null,
                  "channel_slug": null,
                  "match": "exact",
                  "pattern": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                  "normalized": "שבת שלומ שידורינו יתחדשו בצאת השבת",
                  "note": "Shabbat placeholder",
                  "enabled": false,
                  "created_by": "key:Q4mT9xLp",
                  "created_at": "2026-09-25T10:02:11Z",
                  "updated_at": "2026-10-06T09:02:05Z",
                  "excluded": 0
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 8 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — rule not found in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error` — `invalid rule`: match not exact|prefix|contains, pattern empty / over 200 characters / without letters or digits, a prefix|contains pattern under 3 letters after normalising, note over 500 characters, or channel_id not a (non-trashed) channel of this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "invalid rule",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "errors": [
                    {
                      "field": "pattern",
                      "detail": "prefix/contains patterns need at least 3 letters (too broad)"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — catch-up exclusions are not available yet (migration 0058 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      },
      "delete": {
        "tags": [
          "settings"
        ],
        "summary": "Delete a rule (its programmes come back unless their recording was already deleted). Audited.",
        "description": "**Required scope:** `tenant:settings`\n\nRemoves the rule and re-resolves the tenant's excluded set: programmes only this rule excluded return to\ncatch-up, except those whose recording retention has already deleted (they stay listed as `purged`).\nManual decisions are kept. Audited as `catchup.exclusion_rule.delete`. Platform tenants only.",
        "operationId": "deleteCatchupExclusionRule",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — rule not found in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — catch-up exclusions are not available yet (migration 0058 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/tenant/catchup-exclusions/preview": {
      "post": {
        "tags": [
          "settings"
        ],
        "summary": "What a candidate rule would match, without saving it",
        "description": "**Required scope:** `channels:read`\n\nWhat a candidate rule would match, without saving it: upcoming (not ended, next 14 days) and recent (aired in the last 14 days — these would leave catch-up at once)\n\nTakes the same body as creating a rule (validated the same way) and matches it against the tenant's\nprogrammes on non-trashed channels starting within ±14 days; nothing is stored or audited. `upcoming` and\n`recent` are the full counts; `items` lists at most 50 — every upcoming one first (by start), then up to 25\nrecent ones newest first. `included: true` marks a programme a manual include keeps in catch-up anyway.\n`enabled` and `note` play no part. Platform tenants only.",
        "operationId": "previewCatchupExclusionRule",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 8 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CatchupExclusionRuleInput"
              },
              "example": {
                "match": "prefix",
                "pattern": "שבת שלום",
                "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matches",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatchupExclusionPreview"
                },
                "example": {
                  "normalized": "שבת שלומ",
                  "upcoming": 1,
                  "recent": 2,
                  "items": [
                    {
                      "programme_id": "0199c1a0-0b2c-7d3e-8f4a-5b6c7d8e9f01",
                      "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                      "channel_slug": "main",
                      "title": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "start_at": "2026-10-09T14:30:00Z",
                      "end_at": "2026-10-10T16:45:00Z",
                      "included": false
                    },
                    {
                      "programme_id": "0199a8f2-3c4d-7e5f-9a6b-7c8d9e0f1a23",
                      "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                      "channel_slug": "main",
                      "title": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "start_at": "2026-10-02T14:40:00Z",
                      "end_at": "2026-10-03T16:50:00Z",
                      "included": false
                    },
                    {
                      "programme_id": "01998f10-4d5e-7f6a-8b7c-9d0e1f2a3b45",
                      "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                      "channel_slug": "main",
                      "title": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "start_at": "2026-09-25T14:45:00Z",
                      "end_at": "2026-09-26T16:55:00Z",
                      "included": true
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 8 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `invalid rule`: match not exact|prefix|contains, pattern empty / over 200 characters / without letters or digits, a prefix|contains pattern under 3 letters after normalising, or note over 500 characters (channel_id is not checked here: another tenant's channel simply matches nothing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "invalid rule",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "errors": [
                    {
                      "field": "pattern",
                      "detail": "prefix/contains patterns need at least 3 letters (too broad)"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — catch-up exclusions are not available yet (migration 0058 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/tenant/catchup-exclusions/programmes": {
      "get": {
        "tags": [
          "settings"
        ],
        "summary": "The Excluded programmes view",
        "description": "**Required scope:** `channels:read`\n\nThe Excluded programmes view: every excluded programme (rule-matched or manual) with the rule that matched, when its recording will be / was deleted, and programmes a manual include keeps in catch-up although a rule matches them; newest first\n\nLists programmes overlapping the window `from`..`to` (by EPG start/end), newest start first, at most 500\nitems (no cursor — narrow the window or filter by channel/state for more). `state`: `excluded` (out of catch-up,\nrecording still there until `delete_after`), `purged` (recording deleted at `purged_at`; cannot be included\nagain) and `included` (a manual include overrides a rule; `rule_*` names the enabled rule it would otherwise\nmatch). The response echoes the window used. Platform tenants only.",
        "operationId": "listExcludedProgrammes",
        "parameters": [
          {
            "name": "channel_id",
            "in": "query",
            "description": "Only programmes of this channel",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "Only this state (default all three)",
            "schema": {
              "type": "string",
              "enum": [
                "excluded",
                "purged",
                "included"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Window start, RFC 3339 or epoch ms; default now − 35 days (an unparsable value is ignored)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Window end, RFC 3339 or epoch ms; default now + 21 days (an unparsable value is ignored)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{items, from, to}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "from",
                    "to"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ExcludedProgramme"
                      }
                    },
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "programme_id": "0199c1a0-0b2c-7d3e-8f4a-5b6c7d8e9f01",
                      "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                      "channel_slug": "main",
                      "title": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "start_at": "2026-10-09T14:30:00Z",
                      "end_at": "2026-10-10T16:45:00Z",
                      "state": "excluded",
                      "excluded": true,
                      "reason": "rule",
                      "rule_id": "0199b3e2-6a1c-7d2e-9f30-4b5c6d7e8f90",
                      "rule_pattern": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "rule_match": "exact",
                      "excluded_at": "2026-10-06T08:41:30Z",
                      "delete_after": "2026-10-10T17:00:00Z"
                    },
                    {
                      "programme_id": "0199a8f2-3c4d-7e5f-9a6b-7c8d9e0f1a23",
                      "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                      "channel_slug": "main",
                      "title": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "start_at": "2026-10-02T14:40:00Z",
                      "end_at": "2026-10-03T16:50:00Z",
                      "state": "purged",
                      "excluded": true,
                      "reason": "rule",
                      "rule_id": "0199b3e2-6a1c-7d2e-9f30-4b5c6d7e8f90",
                      "rule_pattern": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "rule_match": "exact",
                      "excluded_at": "2026-09-25T10:02:11Z",
                      "delete_after": "2026-10-03T17:05:00Z",
                      "purged_at": "2026-10-03T17:10:42Z"
                    },
                    {
                      "programme_id": "01998f10-4d5e-7f6a-8b7c-9d0e1f2a3b45",
                      "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                      "channel_slug": "main",
                      "title": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "start_at": "2026-09-25T14:45:00Z",
                      "end_at": "2026-09-26T16:55:00Z",
                      "state": "included",
                      "excluded": false,
                      "override": "included",
                      "rule_id": "0199b3e2-6a1c-7d2e-9f30-4b5c6d7e8f90",
                      "rule_pattern": "שבת שלום - שידורינו יתחדשו בצאת השבת",
                      "rule_match": "exact"
                    }
                  ],
                  "from": "2026-09-01T08:45:00Z",
                  "to": "2026-10-27T08:45:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `invalid filter`: state not excluded|purged|included, or channel_id not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — catch-up exclusions are not available yet (migration 0058 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "settings"
        ],
        "summary": "Exclude, include or clear the manual decision of up to 500 programmes",
        "description": "**Required scope:** `tenant:settings`\n\nExclude, include or clear the manual decision of up to 500 programmes. `include` wins over a matching rule (the escape hatch for a false match) and is the Undo of an exclusion while the recording still exists; including a programme whose recording was already deleted is skipped (recording_deleted). Audited (catchup.programme.<action>).\n\nApplies one action to every listed programme of the tenant (duplicate ids are collapsed): `exclude` and\n`include` store a manual decision (with `note`), `clear` drops it so the rules decide again. Unknown ids\n(or another tenant's) are skipped as `not_found`; `include`/`clear` of a programme whose recording was\nalready deleted are skipped as `recording_deleted`. The excluded set is re-resolved before answering, and\neach applied result carries the programme's new `status` (absent = plainly included). One audit entry\n`catchup.programme.<action>` lists the applied and skipped ids. Platform tenants only.",
        "operationId": "setExcludedProgrammes",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 64 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "programme_ids",
                  "action"
                ],
                "additionalProperties": false,
                "properties": {
                  "programme_ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 500,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "exclude",
                      "include",
                      "clear"
                    ]
                  },
                  "note": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "Stored with an exclude/include decision (characters)"
                  }
                }
              },
              "example": {
                "programme_ids": [
                  "01998f10-4d5e-7f6a-8b7c-9d0e1f2a3b45",
                  "0199a8f2-3c4d-7e5f-9a6b-7c8d9e0f1a23"
                ],
                "action": "include",
                "note": "real programme, not the placeholder"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per programme, in request order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "programme_id"
                        ],
                        "properties": {
                          "programme_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "status": {
                            "$ref": "#/components/schemas/CatchupExclusionStatus"
                          },
                          "skipped": {
                            "type": "string",
                            "enum": [
                              "not_found",
                              "recording_deleted"
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "results": [
                    {
                      "programme_id": "01998f10-4d5e-7f6a-8b7c-9d0e1f2a3b45"
                    },
                    {
                      "programme_id": "0199a8f2-3c4d-7e5f-9a6b-7c8d9e0f1a23",
                      "skipped": "recording_deleted"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 64 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `invalid request`: programme_ids empty or over 500, action not exclude|include|clear, or note over 500 characters",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled` — catch-up exclusions are not available yet (migration 0058 not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/billing/statements": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "The tenant's approved monthly usage statements (usage statements, not tax invoices; amounts before VAT)",
        "description": "**Required scope:** `billing:read`\n\nEvery statement Interhost has approved for the tenant, newest month first (at most 500). Drafts and voided\nstatements are never listed. Amounts are decimal strings in the statement's currency, before VAT; `usage` and\n`created_by` are always null here. Fetch one with `GET /v1/billing/statements/{id}` (also as CSV or PDF).",
        "operationId": "listBillingStatements",
        "responses": {
          "200": {
            "description": "Approved statements and the VAT note",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "vat_note"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BillingStatement"
                      }
                    },
                    "vat_note": {
                      "type": "string",
                      "description": "Always \"Usage statement, not a tax invoice. All amounts are before VAT.\""
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0199a3f0-2c11-7b9e-8d4a-6f1e2c3b4a50",
                      "tenant_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "tenant_slug": "now14poc",
                      "tenant_name": "Channel 14 PoC",
                      "month": "2026-09",
                      "status": "approved",
                      "currency": "ILS",
                      "price_list_id": "01990e11-5b20-7c33-9a10-4d2e6f7a8b90",
                      "price_list_name": "PoC 2026",
                      "timezone": "Asia/Jerusalem",
                      "period_from": "2026-08-31T21:00:00Z",
                      "period_to": "2026-09-30T21:00:00Z",
                      "partial": false,
                      "lines": [
                        {
                          "metric": "base_monthly",
                          "quantity": "1",
                          "unit": "month",
                          "unit_price": "2500.00",
                          "amount": "2500.00",
                          "status": "ok"
                        },
                        {
                          "metric": "delivered_tb",
                          "quantity": "18.412",
                          "unit": "TB",
                          "unit_price": "45.00",
                          "amount": "828.54",
                          "status": "ok"
                        },
                        {
                          "metric": "storage_recordings_gb",
                          "quantity": "6120.5",
                          "unit": "GB",
                          "unit_price": "0.08",
                          "amount": "489.64",
                          "status": "ok"
                        }
                      ],
                      "credits": [
                        {
                          "label": "PoC discount",
                          "amount": "500.00"
                        }
                      ],
                      "subtotal": "3818.18",
                      "credits_total": "500.00",
                      "total": "3318.18",
                      "p95_mbps": "412.7",
                      "commit_mbps": null,
                      "usage": null,
                      "notes": null,
                      "created_by": null,
                      "created_at": "2026-10-01T06:00:00Z",
                      "updated_at": "2026-10-02T09:12:44Z",
                      "approved_by": "billing@example.net",
                      "approved_at": "2026-10-02T09:12:44Z",
                      "voided_by": null,
                      "voided_at": null,
                      "void_reason": null
                    }
                  ],
                  "vat_note": "Usage statement, not a tax invoice. All amounts are before VAT."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "billing:read"
      }
    },
    "/v1/billing/statements/{id}": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "One approved statement as JSON, CSV or PDF (lang he or en)",
        "description": "**Required scope:** `billing:read`\n\nOne approved statement of the tenant (drafts, voided statements and other tenants' statements answer 404).\n`format=json` (default) returns the statement, the metric labels (`{metric: [English, Hebrew]}`) and the VAT\nnote. `format=csv` downloads `statement-<tenant>-<month>.csv` (UTF-8 with BOM; one row per line, credit,\nsubtotal, credits total and total, then the VAT note). `format=pdf` downloads the printed statement\n`statement-<tenant>-<month>-<lang>.pdf` in Hebrew (RTL, default) or English (`lang=en`); 503 when PDF rendering\nis not configured, 502 when rendering failed (retry).",
        "operationId": "getBillingStatement",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Statement id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Output format (default json)",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv",
                "pdf"
              ],
              "default": "json"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "description": "PDF language (PDF only; anything but `en` means Hebrew)",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ],
              "default": "he"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Statement (JSON) or a file download (`Content-Disposition: attachment`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "statement",
                    "labels",
                    "vat_note"
                  ],
                  "properties": {
                    "statement": {
                      "$ref": "#/components/schemas/BillingStatement"
                    },
                    "labels": {
                      "type": "object",
                      "description": "metric → [English label, Hebrew label]",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "minItems": 2,
                        "maxItems": 2
                      }
                    },
                    "vat_note": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "statement": {
                    "id": "0199a3f0-2c11-7b9e-8d4a-6f1e2c3b4a50",
                    "tenant_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "tenant_slug": "now14poc",
                    "tenant_name": "Channel 14 PoC",
                    "month": "2026-09",
                    "status": "approved",
                    "currency": "ILS",
                    "price_list_id": null,
                    "price_list_name": null,
                    "timezone": "Asia/Jerusalem",
                    "period_from": "2026-08-31T21:00:00Z",
                    "period_to": "2026-09-30T21:00:00Z",
                    "partial": false,
                    "lines": [
                      {
                        "metric": "base_monthly",
                        "quantity": "1",
                        "unit": "month",
                        "unit_price": "2500.00",
                        "amount": "2500.00",
                        "status": "ok"
                      }
                    ],
                    "credits": [],
                    "subtotal": "2500.00",
                    "credits_total": "0.00",
                    "total": "2500.00",
                    "p95_mbps": null,
                    "commit_mbps": null,
                    "usage": null,
                    "notes": null,
                    "created_by": null,
                    "created_at": "2026-10-01T06:00:00Z",
                    "updated_at": "2026-10-02T09:12:44Z",
                    "approved_by": "billing@example.net",
                    "approved_at": "2026-10-02T09:12:44Z",
                    "voided_by": null,
                    "voided_at": null,
                    "void_reason": null
                  },
                  "labels": {
                    "base_monthly": [
                      "Base monthly fee",
                      "דמי בסיס חודשיים"
                    ],
                    "delivered_tb": [
                      "Delivered traffic",
                      "תעבורה שהוגשה"
                    ]
                  },
                  "vat_note": "Usage statement, not a tax invoice. All amounts are before VAT."
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "tenant,tenant_name,month,statement_id,metric,label,quantity,unit,unit_price,amount,currency,status,note\nnow14poc,Channel 14 PoC,2026-09,0199a3f0-2c11-7b9e-8d4a-6f1e2c3b4a50,base_monthly,Base monthly fee,1,month,2500.00,2500.00,ILS,ok,\n"
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such approved statement in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The PDF could not be rendered; try again",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "PDF rendering is not configured on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "billing:read"
      }
    },
    "/v1/billing/usage": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "Monthly usage of the tenant (read-only)",
        "description": "**Required scope:** `billing:read`\n\nMonthly usage of the tenant (read-only): storage, delivered TB (load tests excluded and shown separately), transcoding and GPU minutes, live channel-days, Sites page views, exported events — each with its source; the selected month also per day. format=csv downloads the same as CSV.\n\n`months` calendar months in the tenant's time zone ending with `month` (oldest first; the current month is\nto date, `partial: true`), and the days of `month` in `daily`. Metric keys: `storage_recordings_gb`,\n`storage_vod_gb`, `storage_images_gb`, `delivered_tb`, `delivered_loadtest_tb` (our own synthetic traffic, not\nbilled; also in `excluded`), `transcode_minutes`, `gpu_minutes` and its parts `gpu_asr_minutes`,\n`gpu_translate_minutes`, `gpu_lipsync_minutes`, `gpu_image_upscale_minutes`, `gpu_video_upscale_minutes`,\n`live_channel_days`, `dual_recording_channel_days`, `site_page_views`, `export_events`, `ssai_impressions_k`\n(server-side stitched ad impressions, thousands), `drm_licences_k` (DRM licences issued, thousands) and `drm_keys`\n(content keys issued over SPEKE/CPIX) — the last two only for tenants that use the DRM service. Each value has a\n`status` (`ok`, `no_data`, `unavailable`); `sources` explains where each figure comes from and `units` its unit.\nStorage is re-measured at most once a day (a call may trigger it). `format=csv` downloads the same table.",
        "operationId": "getBillingUsage",
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-[0-9]{2}$"
            },
            "description": "YYYY-MM in the tenant time zone, not in the future (default: the current month)"
          },
          {
            "name": "months",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12,
              "default": 6
            },
            "description": "how many months ending with month"
          },
          {
            "name": "format",
            "in": "query",
            "description": "json (default) or csv",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ],
              "default": "json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingUsage"
                },
                "example": {
                  "tenant": "now14poc",
                  "timezone": "Asia/Jerusalem",
                  "generated_at": "2026-10-06T08:15:00Z",
                  "plan": {
                    "name": "PoC",
                    "source": "none",
                    "limits": {}
                  },
                  "months": [
                    {
                      "month": "2026-10",
                      "from": "2026-09-30T21:00:00Z",
                      "to": "2026-10-31T22:00:00Z",
                      "partial": true,
                      "metrics": {
                        "delivered_tb": {
                          "value": 3.418,
                          "status": "ok"
                        },
                        "storage_vod_gb": {
                          "value": 1840.2,
                          "status": "ok",
                          "as_of": "2026-10-06T08:15:00Z"
                        },
                        "site_page_views": {
                          "value": null,
                          "status": "no_data"
                        }
                      }
                    }
                  ],
                  "daily": [
                    {
                      "day": "2026-10-01",
                      "metrics": {
                        "delivered_tb": 0.612,
                        "site_page_views": null
                      }
                    }
                  ],
                  "sources": {
                    "delivered_tb": "ClickHouse rollup_traffic_1m …"
                  },
                  "notes": [
                    "Months are calendar months in the tenant time zone; the current month is to date."
                  ],
                  "units": {
                    "delivered_tb": "TB",
                    "storage_vod_gb": "GB",
                    "site_page_views": "views"
                  },
                  "excluded": {
                    "delivered_loadtest_tb": {
                      "value": 0.021,
                      "status": "ok"
                    }
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "period,kind,storage_recordings_gb,storage_vod_gb,…\n,unit,GB,GB,…\n2026-10,month,6120.5,1840.2,…\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Usage is not available on this deployment (`feature_disabled`), or a usage source could not be read (`unavailable`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "billing:read"
      }
    },
    "/v1/billing/usage/hourly": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "Hourly snapshots of the tenant's month-to-date usage meters (read-only)",
        "description": "**Required scope:** `billing:read`\n\nHourly snapshots of the tenant's month-to-date usage meters (read-only) — the statement's own figures taken at the top of every hour, for reconciliation and usage before the month ends\n\nv3 usage-metering. The control plane takes a snapshot a few minutes after every full hour: for each\ntenant, the month-to-date values of the same metrics as `GET /v1/billing/usage` (same keys, `value` + `status`\n+ `as_of`), computed by the same code as the monthly statement. `month` is the tenant-time-zone month the hour\nfalls in, so the first snapshot of a month starts again from zero. Default range: the last 48 hours; at most\n31 days. Snapshots are kept 400 days (a copy is in Insight, ClickHouse `usage_snapshots_1h`).",
        "operationId": "getUsageHourly",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "RFC 3339, inclusive (default: 48 h before `to`)"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "RFC 3339, exclusive (default: now); at most 31 days after `from`"
          }
        ],
        "responses": {
          "200": {
            "description": "The snapshots, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tenant",
                    "from",
                    "to",
                    "items"
                  ],
                  "properties": {
                    "tenant": {
                      "type": "string"
                    },
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "hour",
                          "month",
                          "timezone",
                          "metrics"
                        ],
                        "properties": {
                          "hour": {
                            "type": "string",
                            "format": "date-time",
                            "description": "the snapshot covers the month up to this hour (UTC)"
                          },
                          "month": {
                            "type": "string",
                            "description": "YYYY-MM in the tenant time zone"
                          },
                          "timezone": {
                            "type": "string"
                          },
                          "metrics": {
                            "type": "object",
                            "additionalProperties": true,
                            "description": "metric → {value, status, as_of?} as in GET /v1/billing/usage"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "tenant": "now14poc",
                  "from": "2026-10-05T10:00:00Z",
                  "to": "2026-10-07T10:00:00Z",
                  "items": [
                    {
                      "hour": "2026-10-07T09:00:00Z",
                      "month": "2026-10",
                      "timezone": "Asia/Jerusalem",
                      "metrics": {
                        "delivered_tb": {
                          "value": 3.418,
                          "status": "ok"
                        },
                        "transcode_minutes": {
                          "value": 412,
                          "status": "ok"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Usage is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "billing:read"
      }
    },
    "/v1/images/upscale": {
      "get": {
        "tags": [
          "images"
        ],
        "summary": "AI upscale state of one image of the tenant",
        "description": "**Required scope:** `assets:read`\n\nAI upscale state of one image of the tenant: original and variant URLs, model, scale, timing. status none = never upscaled.\n\nThe upscale state of one image. Poll it after `POST /v1/images/upscale` until `status` leaves `pending`; a job\nthat finished is recorded on read (otherwise the orchestrator records it within ~2 minutes), and a `ready`\nresult switches the Sites image URLs to the variant. An image that was never upscaled answers `status: none`\n(not 404). Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:read`.",
        "operationId": "getImageUpscale",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "description": "Origin path of an image of the tenant: /vod/<tenant>/… or /rec/<tenant>/…,.jpg/.jpeg/.png",
            "schema": {
              "type": "string",
              "maxLength": 512
            },
            "example": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg"
          }
        ],
        "responses": {
          "200": {
            "description": "State",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageUpscale"
                },
                "example": {
                  "source_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                  "original_url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                  "status": "ready",
                  "active": true,
                  "variant_url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261004/0401/1791086470_ai.jpg",
                  "variant": {
                    "id": "0199a8c3-2b10-7e45-9f6a-3c4d5e6f7a8b",
                    "source_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                    "variant_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470_ai.jpg",
                    "status": "ready",
                    "trigger": "manual",
                    "model": "RealESRGAN_x2plus",
                    "scale": 2,
                    "src_w": 1280,
                    "src_h": 720,
                    "out_w": 2560,
                    "out_h": 1440,
                    "timing": {
                      "fetch_ms": 3,
                      "infer_ms": 299,
                      "remote_ms": 446,
                      "upload_ms": 205,
                      "total_ms": 675
                    },
                    "vram_peak_mb": 411,
                    "job_id": "0199a8c3-2b11-7a02-8c3d-4e5f6a7b8c9d",
                    "created_by": "key:k7Qm2xPa",
                    "created_at": "2026-10-04T09:12:40Z",
                    "updated_at": "2026-10-04T09:12:42Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`path` is not a JPEG/PNG under /vod/<tenant>/ or /rec/<tenant>/ (≤ 512 characters, no `..`, `?`, `#`, `\\` or spaces), or it is an `_ai` variant or a channel's `latest` still (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI image upscaling is not available on this deployment (`feature_disabled`; migration not applied or jobs cannot be queued)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "post": {
        "tags": [
          "images"
        ],
        "summary": "Improve one image with AI (Real-ESRGAN, no face restoration)",
        "description": "**Required scope:** `assets:write`\n\nImprove one image with AI (Real-ESRGAN, no face restoration). Idempotent: a queued or finished upscale is returned as is; a reverted one is re-applied without a model run. The original is never modified.\n\nStudio's \"improve quality (AI)\". A `ready` variant, or a `pending` one whose job is still live, is returned as\nis (200 / 202); a `reverted` image with a finished variant is re-activated at once without a model run (200);\notherwise (none, failed, skipped) an `image_upscale` job is queued at standard priority (202) that enlarges x2\neven an image already at the target size (long edge 1920, never wider than 2560). The variant is a separate\nobject `<name>_ai.jpg|png` next to the source; the original is never modified. Follow up with\n`GET /v1/images/upscale?path=…`. Audited as `images.upscale`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "postImageUpscale",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "path"
                ],
                "additionalProperties": false,
                "properties": {
                  "path": {
                    "type": "string",
                    "maxLength": 512,
                    "description": "Origin path of an image of the tenant: /vod/<tenant>/… or /rec/<tenant>/…,.jpg/.jpeg/.png (not an `_ai` variant)"
                  }
                }
              },
              "example": {
                "path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The variant is active (already ready, or a reverted variant re-applied)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageUpscale"
                },
                "example": {
                  "source_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                  "original_url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                  "status": "ready",
                  "active": true,
                  "variant_url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261004/0401/1791086470_ai.jpg",
                  "variant": {
                    "id": "0199a8c3-2b10-7e45-9f6a-3c4d5e6f7a8b",
                    "source_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                    "variant_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470_ai.jpg",
                    "status": "ready",
                    "trigger": "manual",
                    "model": "RealESRGAN_x2plus",
                    "scale": 2,
                    "src_w": 1280,
                    "src_h": 720,
                    "out_w": 2560,
                    "out_h": 1440,
                    "timing": {
                      "fetch_ms": 3,
                      "infer_ms": 299,
                      "remote_ms": 446,
                      "upload_ms": 205,
                      "total_ms": 675
                    },
                    "vram_peak_mb": 411,
                    "job_id": "0199a8c3-2b11-7a02-8c3d-4e5f6a7b8c9d",
                    "created_by": "key:k7Qm2xPa",
                    "created_at": "2026-10-04T09:12:40Z",
                    "updated_at": "2026-10-04T09:12:42Z"
                  }
                }
              }
            }
          },
          "202": {
            "description": "Queued (or already queued/running); poll GET /v1/images/upscale",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageUpscale"
                },
                "example": {
                  "source_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                  "original_url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                  "status": "pending",
                  "active": false,
                  "variant": {
                    "id": "0199a8c3-2b10-7e45-9f6a-3c4d5e6f7a8b",
                    "source_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                    "variant_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470_ai.jpg",
                    "status": "pending",
                    "trigger": "manual",
                    "job_id": "0199a8c3-2b11-7a02-8c3d-4e5f6a7b8c9d",
                    "created_by": "key:k7Qm2xPa",
                    "created_at": "2026-10-04T09:12:40Z",
                    "updated_at": "2026-10-04T09:12:40Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 4 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`path` is not a JPEG/PNG under /vod/<tenant>/ or /rec/<tenant>/ (≤ 512 characters, no `..`, `?`, `#`, `\\` or spaces), or it is an `_ai` variant or a channel's `latest` still (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI image upscaling is not available on this deployment (`feature_disabled`; migration not applied or jobs cannot be queued)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      },
      "delete": {
        "tags": [
          "images"
        ],
        "summary": "Revert to the original (the variant stops being served; it is kept so a later improve needs no model run)",
        "description": "**Required scope:** `assets:write`\n\nStudio's \"back to the original\": the image's variant row becomes `reverted` whatever its status, imgproxy\nserves the original again, and the automatic sweep never upscales this image again. The variant object is kept,\nso a later `POST` re-applies it without a model run. 404 when the image has no upscale row. Audited as\n`images.revert`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "deleteImageUpscale",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "description": "Origin path of an image of the tenant: /vod/<tenant>/… or /rec/<tenant>/…,.jpg/.jpeg/.png",
            "schema": {
              "type": "string",
              "maxLength": 512
            },
            "example": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg"
          }
        ],
        "responses": {
          "200": {
            "description": "Reverted; the state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageUpscale"
                },
                "example": {
                  "source_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                  "original_url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                  "status": "reverted",
                  "active": false,
                  "variant_url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261004/0401/1791086470_ai.jpg",
                  "variant": {
                    "id": "0199a8c3-2b10-7e45-9f6a-3c4d5e6f7a8b",
                    "source_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470.jpg",
                    "variant_path": "/rec/now14poc/main/thumbs/20261004/0401/1791086470_ai.jpg",
                    "status": "reverted",
                    "trigger": "manual",
                    "model": "RealESRGAN_x2plus",
                    "scale": 2,
                    "src_w": 1280,
                    "src_h": 720,
                    "out_w": 2560,
                    "out_h": 1440,
                    "timing": {
                      "fetch_ms": 3,
                      "infer_ms": 299,
                      "remote_ms": 446,
                      "upload_ms": 205,
                      "total_ms": 675
                    },
                    "vram_peak_mb": 411,
                    "job_id": "0199a8c3-2b11-7a02-8c3d-4e5f6a7b8c9d",
                    "created_by": "key:k7Qm2xPa",
                    "created_at": "2026-10-04T09:12:40Z",
                    "updated_at": "2026-10-06T10:03:17Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "This image has no AI version (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`path` is not a JPEG/PNG under /vod/<tenant>/ or /rec/<tenant>/ (≤ 512 characters, no `..`, `?`, `#`, `\\` or spaces), or it is an `_ai` variant or a channel's `latest` still (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI image upscaling is not available on this deployment (`feature_disabled`; migration not applied or jobs cannot be queued)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/posters/{type}/{id}/image": {
      "post": {
        "tags": [
          "posters"
        ],
        "summary": "Upload your own poster (JPEG or PNG body, at most 10 MB, at least 640×360) and choose it",
        "description": "**Required scope:** `assets:write`\n\nThe raw image is the request body (not multipart). The format is detected from the bytes (JPEG or PNG; the\nContent-Type header is not checked); it must be at least 640×360 and at most 10 MB. It is stored as\n`/vod/<tenant>/posters/<type>/<id>/<content hash>.jpg|png` and becomes the editor's choice (locked against\nautomatic changes). Audited as `poster.upload`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "uploadPoster",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "description": "Kind of entity (an unknown type answers 404)",
            "schema": {
              "type": "string",
              "enum": [
                "programme",
                "series",
                "asset",
                "clip"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme, series, asset or clip id of this tenant (a trashed asset answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The image bytes (≤ 10 MB, ≥ 640×360)",
          "content": {
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Poster state with the uploaded image as the editor's choice",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PosterState"
                },
                "example": {
                  "entity_type": "programme",
                  "entity_id": "0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55",
                  "mode": "editor",
                  "current": {
                    "path": "/vod/now14poc/posters/programme/0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55/3f9a1c07b2e4d6a8.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/vod/now14poc/posters/programme/0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55/3f9a1c07b2e4d6a8.jpg"
                  },
                  "auto": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "score": 0.9002,
                    "faces": 1,
                    "eyes_open": 1,
                    "mouth_ok": 1
                  },
                  "editor": {
                    "path": "/vod/now14poc/posters/programme/0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55/3f9a1c07b2e4d6a8.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/vod/now14poc/posters/programme/0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55/3f9a1c07b2e4d6a8.jpg"
                  },
                  "editor_by": "key:k7Qm2xPa",
                  "editor_at": "2026-10-06T08:15:02Z",
                  "candidates": [
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "score": 0.9002,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    },
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "score": 0.8041,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    }
                  ],
                  "scored_at": "2026-10-06T07:05:12Z",
                  "suggest": {
                    "status": "none"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such entity in this tenant, an unknown `type`, or an id that is not a UUID (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The image is larger than 10 MB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The body is not a JPEG or PNG image, or it is smaller than 640×360 (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "Storing the image failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Posters or uploads are not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/posters/{type}/{id}/suggest": {
      "post": {
        "tags": [
          "posters"
        ],
        "summary": "Suggest more poster candidates",
        "description": "**Required scope:** `assets:write`\n\nSuggest more poster candidates — score still-unscored stills of the window and append up to 8 varied extras (more = true)\n\nChange poster-variety. Programmes and clips only (series and assets answer 422). Queues a `poster_select` job\nthat scores up to 60 recorder stills of the same window that were not scored yet and appends up to 8 that do\nnot look like the current candidates (`more: true`; 36 candidates at most in all). The automatic poster, its\nscore and the editor's choice never change. Answers 202 with the poster state (`suggest.status: queued`); poll\n`GET /v1/posters/{type}/{id}` until `suggest.status` is `none` (done) or `failed`. One at a time per entity\n(409). Audited as `poster.suggest`. Not available to CDN-only tenants (403 `feature_disabled`). Scope `assets:write`.",
        "operationId": "suggestPosters",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "description": "programme or clip (other types: 422; unknown: 404)",
            "schema": {
              "type": "string",
              "enum": [
                "programme",
                "clip"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme or clip id of this tenant",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued; the poster state with `suggest.status` = `queued` and its `job_id`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PosterState"
                },
                "example": {
                  "entity_type": "programme",
                  "entity_id": "0199b7d1-5a20-7c3e-8b14-2f6e9d0a1c55",
                  "mode": "auto",
                  "current": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "score": 0.9002,
                    "faces": 1,
                    "eyes_open": 1,
                    "mouth_ok": 1
                  },
                  "auto": {
                    "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                    "score": 0.9002,
                    "faces": 1,
                    "eyes_open": 1,
                    "mouth_ok": 1
                  },
                  "editor": null,
                  "candidates": [
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0632/1791268360.jpg",
                      "score": 0.9002,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    },
                    {
                      "path": "/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "url": "https://cdn.now14-poc.vustream.net/rec/now14poc/main/thumbs/20261006/0633/1791268420.jpg",
                      "score": 0.8041,
                      "faces": 1,
                      "eyes_open": 1,
                      "mouth_ok": 1
                    }
                  ],
                  "scored_at": "2026-10-06T07:05:12Z",
                  "suggest": {
                    "status": "queued",
                    "job_id": "0199ba02-8e31-7f4a-b2c6-0d1e2f3a4b5c"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such entity in this tenant, an unknown `type`, or an id that is not a UUID (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "A suggestion is already queued or running for this entity (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Not a programme or clip, never scored, or no unscored stills left (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Posters are not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/subtitles/settings": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "Hebrew subtitles settings of the tenant",
        "description": "**Required scope:** `assets:read`\n\nHebrew subtitles settings of the tenant: on by default with English translation; the glossary is seeded with financial-news terms\n\nReturns the tenant's subtitles configuration. A tenant that never saved settings gets the defaults: `enabled`,\n`catchup` and `vod` true, `translate: [\"en\"]`, the seeded glossary and glossary translations, and a zero\n`updated_at`. Subtitles are only ever made for finished content (programmes after they end, ready assets),\nnever for the live edge. Requires scope `assets:read`; platform tenants only (403 `feature_disabled` for a\nCDN-only tenant).",
        "operationId": "getSubtitleSettings",
        "responses": {
          "200": {
            "description": "The tenant's settings (or the defaults)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubtitleSettings"
                },
                "example": {
                  "enabled": true,
                  "catchup": true,
                  "vod": true,
                  "glossary": "בנק ישראל\nמדד ת\"א 35\nEBITDA",
                  "translate": [
                    "en"
                  ],
                  "glossary_translations": {
                    "en": "בנק ישראל = Bank of Israel\nמדד ת\"א 35 = TA-35 index",
                    "ru": "בנק ישראל = Банк Израиля"
                  },
                  "foreign_speech": "translate",
                  "updated_at": "2026-09-30T08:14:02Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles are not available (feature not configured or its migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "subtitles"
        ],
        "summary": "Turn subtitles on/off for catch-up and VOD, choose translation languages, edit the glossaries and choose…",
        "description": "**Required scope:** `tenant:settings` or `assets:write`\n\nTurn subtitles on/off for catch-up and VOD, choose translation languages, edit the glossaries and choose how English speech appears\n\nReplaces the tenant's settings: send the whole object — an omitted boolean is saved as false, an omitted\n`glossary` as empty, an omitted `translate` as no translation. `glossary_translations` is merged per language\n(languages not sent keep their stored text). `translate` is stored de-duplicated in player order (en, ru).\nAn omitted `foreign_speech` keeps its stored value. Body at most 16 KiB; unknown fields are rejected (400).\nSubtitles are made for finished content only, never live. Requires scope `tenant:settings`. Audited as\n`subtitles.settings`.\n\n**Editors:** a body with `foreign_speech` alone (`{\"foreign_speech\": \"keep\"}`; an echoed `updated_at` is\nignored) changes only that setting and needs `assets:write` only — how English speech inside a Hebrew\nprogramme appears in the Hebrew subtitles is an editorial choice. Audited as\n`subtitles.settings.foreign_speech` (with the previous value). Any other field needs `tenant:settings` (403\notherwise). The change applies to subtitles made from then on; a finished programme keeps its track until it\nis re-run (POST /v1/subtitles/subjects/{id}/rerun).",
        "operationId": "putSubtitleSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubtitleSettings"
              },
              "examples": {
                "owner": {
                  "summary": "All settings (tenant:settings)",
                  "value": {
                    "enabled": true,
                    "catchup": true,
                    "vod": true,
                    "glossary": "בנק ישראל\nמדד ת\"א 35\nEBITDA",
                    "translate": [
                      "en",
                      "ru"
                    ],
                    "glossary_translations": {
                      "ru": "בנק ישראל = Банк Израиля"
                    },
                    "foreign_speech": "translate"
                  }
                },
                "editor": {
                  "summary": "English speech only (assets:write)",
                  "value": {
                    "foreign_speech": "keep"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the stored settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubtitleSettings"
                },
                "example": {
                  "enabled": true,
                  "catchup": true,
                  "vod": true,
                  "glossary": "בנק ישראל\nמדד ת\"א 35\nEBITDA",
                  "translate": [
                    "en",
                    "ru"
                  ],
                  "glossary_translations": {
                    "ru": "בנק ישראל = Банк Израиля"
                  },
                  "foreign_speech": "translate",
                  "updated_at": "2026-10-06T09:41:17Z"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 16 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error`: glossary over 4000 characters, a glossary translation over 8000, a language other than en/ru, or `foreign_speech` other than translate/keep",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings or assets:write"
      }
    },
    "/v1/summaries/settings": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "Content summaries of the tenant",
        "description": "**Required scope:** `assets:read`\n\nContent summaries of the tenant: a one-paragraph Hebrew summary (≤ 120 words) + presenters per programme / library asset, made from its Hebrew subtitles; off by default\n\nReturns the tenant's switch (off until saved), whether the platform offers summaries at all (`available`;\nSUMMARIES_ENABLED) and the word limit. When on, a `content_summary` job (lowest priority) summarises every\nprogramme and asset that has a ready Hebrew subtitle track. Requires scope `assets:read`; platform tenants only.",
        "operationId": "getSummarySettings",
        "responses": {
          "200": {
            "description": "Settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SummarySettings"
                },
                "example": {
                  "enabled": true,
                  "available": true,
                  "max_words": 120
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or summaries are not available (feature not configured or migration not applied)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "subtitles"
        ],
        "summary": "Turn content summaries on/off for the tenant",
        "description": "**Required scope:** `tenant:settings`\n\nSaves the tenant's switch. Turning it on is refused with 409 `unavailable` while the platform has summaries\noff (`available: false`); turning it off is always allowed. Once on, summaries are made in the background for\nsubjects with ready Hebrew subtitles (no job is queued by this call). Body: exactly `{\"enabled\": true|false}`.\nRequires scope `tenant:settings`. Audited as `summaries.settings`.",
        "operationId": "putSummarySettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SummarySettings"
                },
                "example": {
                  "enabled": true,
                  "available": true,
                  "max_words": 120
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "`unavailable`: summaries are turned off on this platform and cannot be switched on",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the body is not `{\"enabled\": true|false}` (invalid JSON, unknown fields or over 4 KiB included)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or summaries are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/programmes/{id}": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "One programme by id — with its channel, catch-up state and playback URLs",
        "description": "**Required scope:** `channels:read`\n\nThe programme (as in GET /v1/channels/{id}/programmes), its channel (`id`, `slug`, `title`), `catchup` — the\ncatch-up row of GET /v1/channels/{id}/catchup (recorded coverage, status, VOD, playback window, thumbnail;\n`null` while the programme has not started, when it is outside the retention window or excluded from\ncatch-up) — `catchup_exclusion` when an exclusion applies, `in_retention`, and `playback`: `start_over`,\n`live` and, once something is recorded, `catch_up` (open-ended: plays from the programme's start into the\nfollowing programmes and live; protected tenants add a playback token). A programme of another tenant or of a\ntrashed channel is 404. `Cache-Control: no-store`. Not for CDN-only tenants.",
        "operationId": "getProgramme",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The programme",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "programme",
                    "channel",
                    "catchup",
                    "in_retention",
                    "playback"
                  ],
                  "properties": {
                    "programme": {
                      "$ref": "#/components/schemas/Programme"
                    },
                    "channel": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "title": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "catchup": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/CatchupItem"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "catchup_exclusion": {
                      "type": "object",
                      "description": "The exclusion status, when one applies"
                    },
                    "in_retention": {
                      "type": "boolean",
                      "description": "the programme ends inside the channel's recording retention"
                    },
                    "playback": {
                      "type": "object",
                      "properties": {
                        "start_over": {
                          "type": "string",
                          "format": "uri"
                        },
                        "live": {
                          "type": "string",
                          "format": "uri"
                        },
                        "catch_up": {
                          "type": "string",
                          "format": "uri",
                          "description": "present once something of the programme is recorded"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "programme": {
                    "id": "01a1100e-744e-7d06-92f1-adf0be53ee01",
                    "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                    "start_at": "2026-10-06T06:30:00Z",
                    "end_at": "2026-10-06T07:00:00Z",
                    "title": "סוגרים שוק",
                    "source": "epg",
                    "external_id": "redge-81234567",
                    "meta": {
                      "lang": "he",
                      "category": "סוגרים שוק"
                    },
                    "created_at": "2026-10-05T00:00:12Z"
                  },
                  "channel": {
                    "id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                    "slug": "main",
                    "title": "ערוץ 10"
                  },
                  "catchup": {
                    "id": "01a1100e-744e-7d06-92f1-adf0be53ee01",
                    "start_at": "2026-10-06T06:30:00Z",
                    "end_at": "2026-10-06T07:00:00Z",
                    "title": "סוגרים שוק",
                    "source": "epg",
                    "duration_s": 1788,
                    "airing": false,
                    "coverage": 1,
                    "status": "recorded",
                    "vod": null,
                    "adjusted_ms": 53300
                  },
                  "in_retention": true,
                  "playback": {
                    "start_over": "https://cdn.tv10-poc.vustream.net/m/startover/main/01a1100e-744e-7d06-92f1-adf0be53ee01/master.m3u8?c=tv10poc",
                    "live": "https://cdn.tv10-poc.vustream.net/live/tv10poc/main/master.m3u8",
                    "catch_up": "https://cdn.tv10-poc.vustream.net/m/catchup/main/1791181853300/live/master.m3u8?c=tv10poc"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/programmes/{id}/entities": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "Companies mentioned in a programme (company chips), by first mention",
        "description": "**Required scope:** `assets:read`\n\nv3 entity-index. Companies of the tenant's dictionary (GET /v1/entities) mentioned in the programme's\nHebrew transcript: `first_s` is the first mention in seconds from the programme's play start (jump the player\nthere), `count` how many lines mention it, `quote` the first line. A full name counts anywhere; a name that is\nalso an ordinary Hebrew word (טבע, לאומי, הפועלים …) counts only with stock-market words around it (מניית,\nעלתה, אחוז, נסחרת …). Mentions before the play start (the previous programme on the recording) are left out.",
        "operationId": "getProgrammeEntities",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The companies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "programme_id",
                    "items"
                  ],
                  "properties": {
                    "programme_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CompanyChip"
                      }
                    }
                  }
                },
                "example": {
                  "programme_id": "01a10b38-64de-754e-9e21-aa18126ebb77",
                  "items": [
                    {
                      "slug": "amazon",
                      "name": "אמזון",
                      "name_en": "Amazon",
                      "ticker": "AMZN",
                      "exchange": "NASDAQ",
                      "first_s": 812.4,
                      "count": 1,
                      "quote": "של ארביטריישן אוטומטי, באמזון, באי-ביי,"
                    },
                    {
                      "slug": "el-al",
                      "name": "אל על",
                      "name_en": "El Al",
                      "ticker": "ELAL",
                      "exchange": "TASE",
                      "first_s": 2390.1,
                      "count": 2,
                      "quote": "להמריא, ומתי מניית אל על תחצה את 20"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/programmes/{id}/mentions": {
      "post": {
        "tags": [
          "subtitles"
        ],
        "summary": "Re-index a programme's transcript now (moment search + company mentions)",
        "description": "**Required scope:** `assets:write`\n\nReads the programme's ready Hebrew track again and replaces its moment-search lines and company mentions (the\nsweep does this by itself within minutes of a new or edited track; this is for an editor who just fixed the\nsubtitles or the dictionary). Answers the programme's companies as GET …/entities does, plus the counts.",
        "operationId": "reindexProgrammeMentions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Indexed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "programme_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "cues": {
                      "type": "integer"
                    },
                    "mentions": {
                      "type": "integer"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CompanyChip"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The programme has no ready Hebrew subtitles yet (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/entities": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "The tenant's company dictionary, with mention counts",
        "description": "**Required scope:** `assets:read`\n\nv3 entity-index: the companies (and later people) the transcripts are matched against — Hebrew and\nEnglish names, `aliases` (count anywhere), `ambiguous` aliases (count only with stock-market words around them),\nticker and exchange — and how many lines mention each. Seeded with ≈55 TASE / US companies by\n`viewstream admin seed-entities --tenant <slug>`.",
        "operationId": "listEntities",
        "responses": {
          "200": {
            "description": "The dictionary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "company",
                              "person",
                              "ticker"
                            ]
                          },
                          "slug": {
                            "type": "string"
                          },
                          "name_he": {
                            "type": "string"
                          },
                          "name_en": {
                            "type": "string"
                          },
                          "aliases": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "ambiguous": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "ticker": {
                            "type": "string"
                          },
                          "exchange": {
                            "type": "string",
                            "enum": [
                              "TASE",
                              "NASDAQ",
                              "NYSE"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "confirmed",
                              "unconfirmed",
                              "disabled"
                            ]
                          },
                          "mentions": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01a12000-0000-7000-8000-0000000000e1",
                      "kind": "company",
                      "slug": "teva",
                      "name_he": "טבע",
                      "name_en": "Teva Pharmaceutical",
                      "aliases": [
                        "טבע תעשיות פרמצבטיות",
                        "teva"
                      ],
                      "ambiguous": [
                        "טבע"
                      ],
                      "ticker": "TEVA",
                      "exchange": "NYSE",
                      "status": "confirmed",
                      "mentions": 12
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/programmes/{id}/summary": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "The programme's Hebrew summary and presenters, with its status",
        "description": "**Required scope:** `assets:read`\n\nReturns the stored summary combined with the state of the newest `content_summary` job (see ContentSummary\nfor the statuses). `hebrew_ready` tells whether a source exists yet; `enabled` reflects the tenant and\nplatform switches. A programme without a summary answers 200 with `status: none` and an empty summary.\nRequires scope `assets:read`; platform tenants only.",
        "operationId": "getProgrammeSummary",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id (any programme of the tenant's channels)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContentSummary"
                },
                "example": {
                  "subject_type": "programme",
                  "subject_id": "01a0e6ff-6356-75db-924b-0c8428f727ae",
                  "enabled": true,
                  "hebrew_ready": true,
                  "status": "ready",
                  "summary": "המהדורה נפתחת בהחלטת בנק ישראל להשאיר את הריבית ללא שינוי, ובהשלכותיה על שוק המשכנתאות. בהמשך: סקירת המסחר בבורסה בתל אביב ותחזית האינפלציה לרבעון הבא.",
                  "presenters": [
                    "דנה לוי"
                  ],
                  "presenters_source": "transcript",
                  "topics": [
                    "ריבית",
                    "בנק ישראל",
                    "שוק ההון"
                  ],
                  "words": 31,
                  "max_words": 120,
                  "source_version": 1,
                  "model": "large+hebrew",
                  "updated_at": "2026-10-05T21:12:40Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: programme not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or summaries are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "subtitles"
        ],
        "summary": "Edit the programme's summary and presenters (an edit is replaced only by regenerate)",
        "description": "**Required scope:** `assets:write`\n\nSaves the summary and presenters as an edit (`status: edited`, `presenters_source: editor`); the background\nsweep never replaces an edited summary, only POST …/summary/regenerate does. Both fields are replaced: an\nomitted `summary` saves an empty text, omitted `presenters` none. The summary is trimmed and may have at most\n120 words and 8000 characters; presenters are trimmed, empty names dropped, at most 10 names of at most 80\ncharacters. Works whether or not the tenant switch is on. Body at most 64 KiB. Requires scope `assets:write`.\nAudited as `summary.edit`.",
        "operationId": "putProgrammeSummary",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id (any programme of the tenant's channels)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "summary": {
                    "type": "string",
                    "maxLength": 8000,
                    "description": "Hebrew, at most 120 words"
                  },
                  "presenters": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "maxLength": 80
                    }
                  }
                }
              },
              "example": {
                "summary": "המהדורה נפתחת בהחלטת בנק ישראל להשאיר את הריבית ללא שינוי. בהמשך: סקירת המסחר בבורסה.",
                "presenters": [
                  "דנה לוי",
                  "יואב כהן"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the summary as now stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContentSummary"
                },
                "example": {
                  "subject_type": "programme",
                  "subject_id": "01a0e6ff-6356-75db-924b-0c8428f727ae",
                  "enabled": true,
                  "hebrew_ready": true,
                  "status": "edited",
                  "summary": "המהדורה נפתחת בהחלטת בנק ישראל להשאיר את הריבית ללא שינוי. בהמשך: סקירת המסחר בבורסה.",
                  "presenters": [
                    "דנה לוי",
                    "יואב כהן"
                  ],
                  "presenters_source": "editor",
                  "topics": [
                    "ריבית",
                    "בנק ישראל"
                  ],
                  "words": 15,
                  "max_words": 120,
                  "source_version": 1,
                  "model": "large+hebrew",
                  "edited_by": "key:vs7Hq2Lm",
                  "edited_at": "2026-10-06T09:55:03Z",
                  "updated_at": "2026-10-06T09:55:03Z"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 64 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: programme not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or summaries are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/programmes/{id}/summary/regenerate": {
      "post": {
        "tags": [
          "subtitles"
        ],
        "summary": "Make the programme's summary again from its current Hebrew subtitles (replaces an edited summary)",
        "description": "**Required scope:** `assets:write`\n\nQueues a `content_summary` job now (lowest priority) that summarises the current ready Hebrew track and, when\nit finishes, replaces the stored summary — including an edited one. Answers 202 with the job id; follow it with\nGET …/summary (`status` pending → running → ready) or GET /v1/jobs/{id}. Refused with 409 while the\nplatform has summaries off, while a summary job for the programme is queued or running, or when no job can be made\n(no ready Hebrew subtitles yet — also the answer while the tenant's summaries switch is off). Requires scope\n`assets:write`. Audited as `summary.regenerate`.",
        "operationId": "regenerateProgrammeSummary",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id (any programme of the tenant's channels)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "status"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    }
                  }
                },
                "example": {
                  "job_id": "01a0f2b4-77c1-7e08-a4d3-5b9e1c2f3a40",
                  "status": "pending"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: programme not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`unavailable` (summaries off on the platform) or `conflict` (a summary is already being made; no ready Hebrew subtitles for this programme yet)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or summaries are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/programmes/{id}/clip-candidates": {
      "post": {
        "tags": [
          "subtitles"
        ],
        "summary": "Propose vertical clips from the programme's Hebrew transcript (45-60 s each, with a Hebrew title)",
        "description": "**Required scope:** `assets:write`\n\nQueues an `ai_clip_candidates` job (ai-clips): the ready Hebrew track of the ended\nprogramme, within its playback window, is ranked by the platform's language model through the Behema gateway\n(an offload route when configured; a text heuristic when no model answers), each pick snapped to whole\nsentences of 45–60 s, and a short Hebrew title written for each. The clips appear in GET …/clip-candidates as\nAI artefacts with status `draft` (provenance records the model and route that served). Answers 202 with the\njob id. Requires scope `assets:write`. Audited as `ai.clips.candidates`.",
        "operationId": "postClipCandidates",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id (any programme of the tenant's channels)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "count": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 8,
                    "default": 5,
                    "description": "clips wanted"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "job"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "job": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: programme not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`not_ready` (the programme has not ended, or has no ready Hebrew subtitles) or `busy` (candidates are already being prepared)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID, or count is not 1–8",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or AI artefacts are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      },
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "The programme's vertical clips (candidates and renders) and the state of the latest candidates job",
        "description": "**Required scope:** `assets:read`\n\n`job` is the latest `ai_clip_candidates` job of the programme (`none`, `queued`, `running`, `done`, `failed`\nwith `error`). `clips` are its AI artefacts of kind `clip`, newest first; `render` appears once a render was\nasked for, with `url` / `poster_url` (the tenant's delivery hostname) when ready. Requires scope `assets:read`.",
        "operationId": "getClipCandidates",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The clips",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job",
                    "clips"
                  ],
                  "properties": {
                    "job": {
                      "type": "string",
                      "enum": [
                        "none",
                        "queued",
                        "running",
                        "done",
                        "failed"
                      ]
                    },
                    "error": {
                      "type": "string"
                    },
                    "clips": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AIClip"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: programme not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or AI artefacts are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/ai/clips/{id}/render": {
      "post": {
        "tags": [
          "subtitles"
        ],
        "summary": "Render a vertical clip: 1080×1920 MP4 with burned-in Hebrew captions and the tenant's title card",
        "description": "**Required scope:** `assets:write`\n\nQueues an `ai_render_clip` job for one clip artefact: its range of the recording (the channel's top rendition up\nto 1080p and its AAC rendition), a title card with the logo and colours of the tenant's site theme, the picture\nover a blurred fill, the show and title above it and the cues of the track below as right-to-left captions; plus\na poster JPEG. The render is the `vertical-9x16` variant of a real clip (GET /v1/clips): the artefact's linked\nclip, else a clip of the channel with exactly the same range, else a new frame-precise clip made exactly like\nPOST /v1/clips (clip.ready now, clip.final when it is finalised — with `variants` once the render is ready; a\nrender that ends after the clip is final sends one more clip.final with `variant: vertical-9x16`). The files go\nunder the clip and are deleted with it. Follow it with GET /v1/programmes/{id}/clip-candidates (`render.status`\nqueued → running → ready) or GET /v1/clips/{clip_id} (`variants`). Requires scope `assets:write`, and\n`clips:write` when a new clip has to be made. Audited as `ai.clips.render` (and `clip.create`).",
        "operationId": "postRenderClip",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Clip artefact id (from GET …/clip-candidates)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "render",
                    "clip_id"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "render": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    },
                    "clip_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "the /v1/clips clip the render becomes a variant of"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_scope`: assets:write, or clips:write when a new clip has to be made",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no such clip for this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`busy` (the clip is already rendering) or `not_ready` (no ready Hebrew subtitles)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID, or the clip cannot be rendered",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: AI artefacts are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}/summary": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "The asset's Hebrew summary and presenters, with its status",
        "description": "**Required scope:** `assets:read`\n\nReturns the stored summary combined with the state of the newest `content_summary` job (see ContentSummary\nfor the statuses). `hebrew_ready` tells whether a source exists yet; `enabled` reflects the tenant and\nplatform switches. A asset without a summary answers 200 with `status: none` and an empty summary.\nRequires scope `assets:read`; platform tenants only.",
        "operationId": "getAssetSummary",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id (not in the trash)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContentSummary"
                },
                "example": {
                  "subject_type": "asset",
                  "subject_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                  "enabled": true,
                  "hebrew_ready": true,
                  "status": "ready",
                  "summary": "המהדורה נפתחת בהחלטת בנק ישראל להשאיר את הריבית ללא שינוי, ובהשלכותיה על שוק המשכנתאות. בהמשך: סקירת המסחר בבורסה בתל אביב ותחזית האינפלציה לרבעון הבא.",
                  "presenters": [
                    "דנה לוי"
                  ],
                  "presenters_source": "transcript",
                  "topics": [
                    "ריבית",
                    "בנק ישראל",
                    "שוק ההון"
                  ],
                  "words": 31,
                  "max_words": 120,
                  "source_version": 1,
                  "model": "large+hebrew",
                  "updated_at": "2026-10-05T21:12:40Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: asset not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or summaries are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "subtitles"
        ],
        "summary": "Edit the asset's summary and presenters (an edit is replaced only by regenerate)",
        "description": "**Required scope:** `assets:write`\n\nSaves the summary and presenters as an edit (`status: edited`, `presenters_source: editor`); the background\nsweep never replaces an edited summary, only POST …/summary/regenerate does. Both fields are replaced: an\nomitted `summary` saves an empty text, omitted `presenters` none. The summary is trimmed and may have at most\n120 words and 8000 characters; presenters are trimmed, empty names dropped, at most 10 names of at most 80\ncharacters. Works whether or not the tenant switch is on. Body at most 64 KiB. Requires scope `assets:write`.\nAudited as `summary.edit`.",
        "operationId": "putAssetSummary",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id (not in the trash)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "summary": {
                    "type": "string",
                    "maxLength": 8000,
                    "description": "Hebrew, at most 120 words"
                  },
                  "presenters": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "maxLength": 80
                    }
                  }
                }
              },
              "example": {
                "summary": "המהדורה נפתחת בהחלטת בנק ישראל להשאיר את הריבית ללא שינוי. בהמשך: סקירת המסחר בבורסה.",
                "presenters": [
                  "דנה לוי",
                  "יואב כהן"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the summary as now stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContentSummary"
                },
                "example": {
                  "subject_type": "asset",
                  "subject_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                  "enabled": true,
                  "hebrew_ready": true,
                  "status": "edited",
                  "summary": "המהדורה נפתחת בהחלטת בנק ישראל להשאיר את הריבית ללא שינוי. בהמשך: סקירת המסחר בבורסה.",
                  "presenters": [
                    "דנה לוי",
                    "יואב כהן"
                  ],
                  "presenters_source": "editor",
                  "topics": [
                    "ריבית",
                    "בנק ישראל"
                  ],
                  "words": 15,
                  "max_words": 120,
                  "source_version": 1,
                  "model": "large+hebrew",
                  "edited_by": "key:vs7Hq2Lm",
                  "edited_at": "2026-10-06T09:55:03Z",
                  "updated_at": "2026-10-06T09:55:03Z"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 64 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: asset not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or summaries are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}/summary/regenerate": {
      "post": {
        "tags": [
          "subtitles"
        ],
        "summary": "Make the asset's summary again from its current Hebrew subtitles (replaces an edited summary)",
        "description": "**Required scope:** `assets:write`\n\nQueues a `content_summary` job now (lowest priority) that summarises the current ready Hebrew track and, when\nit finishes, replaces the stored summary — including an edited one. Answers 202 with the job id; follow it with\nGET …/summary (`status` pending → running → ready) or GET /v1/jobs/{id}. Refused with 409 while the\nplatform has summaries off, while a summary job for the asset is queued or running, or when no job can be made\n(no ready Hebrew subtitles yet — also the answer while the tenant's summaries switch is off). Requires scope\n`assets:write`. Audited as `summary.regenerate`.",
        "operationId": "regenerateAssetSummary",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id (not in the trash)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "status"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    }
                  }
                },
                "example": {
                  "job_id": "01a0f2b4-77c1-7e08-a4d3-5b9e1c2f3a40",
                  "status": "pending"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: asset not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`unavailable` (summaries off on the platform) or `conflict` (a summary is already being made; no ready Hebrew subtitles for this asset yet)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or summaries are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/programmes/{id}/article": {
      "post": {
        "tags": [
          "ai"
        ],
        "summary": "Write an article draft of the programme from its Hebrew subtitles",
        "description": "**Required scope:** `assets:write`\n\nQueues an `ai_article` job: from the programme's ready Hebrew subtitles the language models write a Hebrew\narticle — headline (≤ 70 characters), standfirst (≤ 160), summary (≤ 120 words), chapters with their times,\n3 quotes (verbatim runs of the transcript, with their moment), names mentioned and tags. Every number in the\nHebrew text is checked against the transcript; names are kept only when they occur in it. A quote carries\nthe speaker's role (presenter, guest, report), never a guessed name. The result is an `article` artefact in\nstate `draft` in the review queue (GET /v1/ai/artifacts) within about 10 minutes; it reaches the site and\nwebhooks only when an editor publishes it. `offload: true` runs every step on the platform's offload route —\na third-party model that then sees the transcript (off unless Interhost enabled one). Requires scope\n`assets:write`. Audited as `ai.article.requested`.",
        "operationId": "postProgrammeArticle",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id (any programme of the tenant's channels)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "offload": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "status",
                    "subject_type",
                    "subject_id"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    },
                    "subject_type": {
                      "type": "string",
                      "enum": [
                        "programme",
                        "asset"
                      ]
                    },
                    "subject_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                },
                "example": {
                  "job_id": "01a11a40-2c11-7d55-9b1e-6f0d2a4b8c31",
                  "status": "queued",
                  "subject_type": "programme",
                  "subject_id": "01a10b38-64e9-7047-9e50-0c8f4c7a4fb2"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: programme not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`not_ready`: no ready Hebrew subtitles yet; `conflict`: an article of this programme is already being written",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles or AI artefacts are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      },
      "get": {
        "tags": [
          "ai"
        ],
        "summary": "The programme's newest article job and article artefact",
        "description": "**Required scope:** `ai:read`\n\nThe newest `ai_article` job (`queued`, `dispatched`, `running`, `succeeded`, `failed`, with a redacted error)\nand the newest `article` artefact of the programme in any state (null when none). Requires scope `ai:read`.",
        "operationId": "getProgrammeArticle",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job and artefact",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "subject_type",
                    "subject_id",
                    "job",
                    "artifact"
                  ],
                  "properties": {
                    "subject_type": {
                      "type": "string"
                    },
                    "subject_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "job": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string"
                        },
                        "at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "error": {
                          "type": "string"
                        }
                      }
                    },
                    "artifact": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/AIArtifact"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: programme not found (for this tenant)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:read"
      }
    },
    "/v1/assets/{id}/article": {
      "post": {
        "tags": [
          "ai"
        ],
        "summary": "Write an article draft of the library video from its Hebrew subtitles",
        "description": "**Required scope:** `assets:write`\n\nAs POST /v1/programmes/{id}/article, for a library video (times count from the start of the video). Requires scope `assets:write`.",
        "operationId": "postAssetArticle",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "offload": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued: {job_id, status, subject_type, subject_id}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`not_ready` or `conflict`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      },
      "get": {
        "tags": [
          "ai"
        ],
        "summary": "The library video's newest article job and article artefact",
        "description": "**Required scope:** `ai:read`\n\nAs GET /v1/programmes/{id}/article, for a library video. Requires scope `ai:read`.",
        "operationId": "getAssetArticle",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{subject_type, subject_id, job, artifact}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:read"
      }
    },
    "/v1/programmes/{id}/artifacts": {
      "get": {
        "tags": [
          "ai"
        ],
        "summary": "The programme's AI artefacts (every state)",
        "description": "**Required scope:** `ai:read`\n\nAll AI artefacts of the programme, newest first, optionally of one `kind`. Requires scope `ai:read`.",
        "operationId": "listProgrammeArtifacts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "article",
                "clip",
                "chips",
                "summary"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Artefacts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AIArtifact"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:read"
      }
    },
    "/v1/assets/{id}/artifacts": {
      "get": {
        "tags": [
          "ai"
        ],
        "summary": "The library video's AI artefacts (every state)",
        "description": "**Required scope:** `ai:read`\n\nAs GET /v1/programmes/{id}/artifacts, for a library video. Requires scope `ai:read`.",
        "operationId": "listAssetArtifacts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "article",
                "clip",
                "chips",
                "summary"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{items: [AIArtifact]}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:read"
      }
    },
    "/v1/ai/artifacts": {
      "get": {
        "tags": [
          "ai"
        ],
        "summary": "The review queue: the tenant's AI artefacts",
        "description": "**Required scope:** `ai:read`\n\nAI artefacts of the tenant, newest first, filtered by `status` and `kind`; a page of `limit` (default 50, at\nmost 100) items with `next_before` when there may be more (pass it as `before`). Every item carries its\n`label` (the AI label to show: reviewed by an editor or automatic). The payload of an item is the same as in\nthe `artifact.published` webhook. Requires scope `ai:read`.",
        "operationId": "listAIArtifacts",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "approved",
                "rejected",
                "published"
              ]
            }
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "article",
                "clip",
                "chips",
                "summary"
              ]
            }
          },
          {
            "name": "before",
            "in": "query",
            "description": "next_before of the previous page",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of artefacts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AIArtifact"
                      }
                    },
                    "next_before": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: AI artefacts are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "ai:read"
      }
    },
    "/v1/ai/artifacts/{id}": {
      "get": {
        "tags": [
          "ai"
        ],
        "summary": "One AI artefact",
        "description": "**Required scope:** `ai:read`\n\nRequires scope `ai:read`.",
        "operationId": "getAIArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The artefact",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIArtifact"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:read"
      },
      "patch": {
        "tags": [
          "ai"
        ],
        "summary": "Edit an AI artefact before it is published",
        "description": "**Required scope:** `ai:write`\n\nAn editor's change of a draft (or approved) artefact: `title` and / or top-level `body` fields, merged (e.g.\n`{\"body\": {\"headline\": \"…\", \"standfirst\": \"…\"}}`; for an article a new `headline` also becomes the title).\nEach changed field is stored as a correction (before, after, who, when) in `provenance.corrections`. A\npublished or rejected artefact cannot be edited. Requires scope `ai:write`. Audited as `ai.artifact.edited`.",
        "operationId": "patchAIArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "body": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "top-level body fields to replace"
                  }
                }
              },
              "example": {
                "body": {
                  "headline": "תקציב הביטחון מתקרב ל-200 מיליארד שקל",
                  "standfirst": "יחס החוב לתוצר עומד על 69%"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The artefact as now stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIArtifact"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`conflict`: not a draft any more",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:write"
      }
    },
    "/v1/ai/artifacts/{id}/approve": {
      "post": {
        "tags": [
          "ai"
        ],
        "summary": "Approve and publish an AI artefact (fires artifact.published)",
        "description": "**Required scope:** `ai:publish`\n\nAn editor's approval publishes the artefact (editorial-review): `status: published`, the label reads\n\"reviewed by an editor\", the tenant's site shows it and the `artifact.published` webhook fires once (a repeated\ncall changes nothing). A rejected artefact cannot be published. Requires scope `ai:publish`. Audited as\n`ai.artifact.published`.",
        "operationId": "approveAIArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The published artefact",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIArtifact"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`conflict`: the artefact was rejected",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:publish"
      }
    },
    "/v1/ai/artifacts/{id}/publish": {
      "post": {
        "tags": [
          "ai"
        ],
        "summary": "Publish an AI artefact (same as approve)",
        "description": "**Required scope:** `ai:publish`\n\nThe same as POST …/approve. Requires scope `ai:publish`.",
        "operationId": "publishAIArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The published artefact",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIArtifact"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`conflict`: the artefact was rejected",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:publish"
      }
    },
    "/v1/ai/artifacts/{id}/reject": {
      "post": {
        "tags": [
          "ai"
        ],
        "summary": "Reject an AI artefact with a reason (takes a published one off the site)",
        "description": "**Required scope:** `ai:write`\n\nRejects a draft, approved or published artefact; the reason is stored and it leaves every surface. Requires\nscope `ai:write`. Audited as `ai.artifact.rejected`.",
        "operationId": "rejectAIArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 1000
                  }
                }
              },
              "example": {
                "reason": "הכותרת לא מדויקת"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rejected artefact",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIArtifact"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`conflict`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "ai:write"
      }
    },
    "/v1/subtitles/tracks": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "Subtitle tracks of the tenant in every language, newest first",
        "description": "**Required scope:** `assets:read`\n\nSubtitle tracks of the tenant in every language, newest first; source asr | mt (translated from Hebrew) | edited | uploaded\n\nLists the tenant's subtitle tracks (all languages, all subjects or one), newest first. There is no cursor: one\npage of at most `limit` tracks. `vtt_url` points at the WebVTT on the tenant's CDN hostname; it changes with\nevery new version (re-run or edit). Translations carry `source_version`, `model` and `stats`. Requires scope\n`assets:read`; platform tenants only.",
        "operationId": "listSubtitleTracks",
        "parameters": [
          {
            "name": "subject_id",
            "in": "query",
            "description": "Only the tracks of this programme, asset or clip",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Tracks to return; values outside 1–200 fall back to 50",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tracks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SubtitleTrack"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01a0e7c2-5b14-7d3e-9f21-6c8a0b3d4e5f",
                      "subject_type": "programme",
                      "subject_id": "01a0e6ff-6356-75db-924b-0c8428f727ae",
                      "lang": "en",
                      "name": "English",
                      "status": "ready",
                      "version": 1,
                      "source": "mt",
                      "source_version": 1,
                      "model": "large",
                      "stats": {
                        "cues": 357,
                        "requests": 8,
                        "prompt_tokens": 16556,
                        "completion_tokens": 19878,
                        "over_cps": 8
                      },
                      "start_at": "2026-09-29T17:00:00Z",
                      "end_at": "2026-09-29T17:30:00Z",
                      "word_level": false,
                      "cues": 357,
                      "words": 3810,
                      "low_conf_cues": 8,
                      "rtf": null,
                      "vtt_url": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/subs/en/20260929/01a0e6ff-6356-75db-924b-0c8428f727ae/s1/en.vtt",
                      "created_at": "2026-09-29T17:44:21Z"
                    },
                    {
                      "id": "01a0e7a8-0c33-7f1a-8b6e-2d4f6a8c0e12",
                      "subject_type": "programme",
                      "subject_id": "01a0e6ff-6356-75db-924b-0c8428f727ae",
                      "lang": "he",
                      "name": "עברית",
                      "status": "ready",
                      "version": 1,
                      "source": "asr",
                      "model": "asr-he",
                      "start_at": "2026-09-29T17:00:00Z",
                      "end_at": "2026-09-29T17:30:00Z",
                      "word_level": true,
                      "cues": 357,
                      "words": 3402,
                      "low_conf_cues": 5,
                      "rtf": 0.018,
                      "vtt_url": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/subs/he/20260929/01a0e6ff-6356-75db-924b-0c8428f727ae/he.vtt",
                      "created_at": "2026-09-29T17:38:05Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error`: subject_id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/subtitles/subjects/{id}": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "Subtitle languages of one programme, asset or clip",
        "description": "**Required scope:** `assets:read`\n\nSubtitle languages of one programme, asset or clip: Hebrew plus the tenant's translation languages, each with its state and track\n\nOne entry per language — Hebrew, the tenant's `translate` languages and any other language that has a track —\nin player order. `status`: `ready` / `failed` / `disabled` (the track's own state), `outdated` (a machine\ntranslation made from an older Hebrew version), `queued` / `running` (the newest job), `failed` (the job failed\nand no track exists; `error` holds the redacted job error), `waiting` (no track or job yet), `waiting_service`\n(the translation model is unavailable; resumes by itself) or `off` for every language while `auto` is false\n(PUT …/auto). The id is not checked for existence: an unknown id answers 200 with every language `waiting`.\nRequires scope `assets:read`; platform tenants only.",
        "operationId": "getSubtitleSubject",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme, asset or clip id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Languages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "subject_id",
                    "enabled",
                    "auto",
                    "languages"
                  ],
                  "properties": {
                    "subject_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "enabled": {
                      "type": "boolean",
                      "description": "the tenant's subtitles master switch"
                    },
                    "auto": {
                      "type": "boolean",
                      "description": "automatic subtitles for this video (default true)"
                    },
                    "languages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "lang",
                          "name",
                          "status"
                        ],
                        "properties": {
                          "lang": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "ready",
                              "outdated",
                              "queued",
                              "running",
                              "failed",
                              "waiting",
                              "waiting_service",
                              "off",
                              "disabled"
                            ]
                          },
                          "error": {
                            "type": "string",
                            "description": "the newest failed job's error, redacted (omitted otherwise)"
                          },
                          "track": {
                            "$ref": "#/components/schemas/SubtitleTrack"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "subject_id": "01a0e6ff-6356-75db-924b-0c8428f727ae",
                  "enabled": true,
                  "auto": true,
                  "languages": [
                    {
                      "lang": "he",
                      "name": "עברית",
                      "status": "ready",
                      "track": {
                        "id": "01a0e7a8-0c33-7f1a-8b6e-2d4f6a8c0e12",
                        "subject_type": "programme",
                        "subject_id": "01a0e6ff-6356-75db-924b-0c8428f727ae",
                        "lang": "he",
                        "name": "עברית",
                        "status": "ready",
                        "version": 2,
                        "source": "edited",
                        "model": "asr-he",
                        "start_at": "2026-09-29T17:00:00Z",
                        "end_at": "2026-09-29T17:30:00Z",
                        "word_level": true,
                        "cues": 356,
                        "words": 3398,
                        "low_conf_cues": 0,
                        "rtf": 0.018,
                        "vtt_url": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/subs/he/20260929/01a0e6ff-6356-75db-924b-0c8428f727ae/e2/he.vtt",
                        "created_at": "2026-09-29T17:38:05Z"
                      }
                    },
                    {
                      "lang": "en",
                      "name": "English",
                      "status": "running"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/subtitles/subjects/{id}/rerun": {
      "post": {
        "tags": [
          "subtitles"
        ],
        "summary": "Queue a new Hebrew recognition of a finished programme or ready asset (new track version; translations follow)",
        "description": "**Required scope:** `assets:write`\n\nQueues a `subtitles` job (bulk priority) that recognises the Hebrew audio again. The id is a programme that\nhas ended, is still inside its channel's recording and is not excluded from catch-up, or a ready, non-deleted\nasset of the tenant. The result is a new track version under new object keys (new `vtt_url`); the machine\ntranslations are re-made from it automatically. Follow progress with GET /v1/subtitles/subjects/{id} or the\njobs API. Refused with 409 when the Hebrew track was edited (an edit is never replaced) or a subtitles job for\nthe subject is already queued or running. The re-run uses the tenant's current settings — e.g. a changed\n`foreign_speech` (English speech shown in English or translated into Hebrew) applies to a programme only after\nit is re-run. Requires scope `assets:write`. Audited as `subtitles.rerun`.",
        "operationId": "rerunSubtitles",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme or asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "queued",
                    "subject_id"
                  ],
                  "properties": {
                    "queued": {
                      "type": "boolean",
                      "const": true
                    },
                    "subject_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                },
                "example": {
                  "queued": true,
                  "subject_id": "01a0e6ff-6356-75db-924b-0c8428f727ae"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no finished programme or ready asset with this id",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`: the Hebrew track was edited in Studio, or a subtitles job is already in progress",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID, or the subject cannot be subtitled (no recorded audio for the programme, no audio rendition for the asset, jobs unavailable)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/subtitles/subjects/{id}/auto": {
      "put": {
        "tags": [
          "subtitles"
        ],
        "summary": "Automatic subtitles for one programme, video or clip on/off (default on)",
        "description": "**Required scope:** `assets:write`\n\nOff: the subject's ready tracks become `disabled` (players stop listing them, static VOD masters lose the\nrenditions) and no subtitles job is queued for it. On: the disabled tracks are `ready` again and missing ones\nare made by the regular sweep. Idempotent. The id must be a programme, a non-deleted asset or a clip of the\ntenant. Body: exactly `{\"enabled\": true|false}` (at most 1 KiB). Requires scope `assets:write`. Audited as\n`subtitles.auto`.",
        "operationId": "setSubtitlesAuto",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Programme, asset or clip id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "subject_id",
                    "kind",
                    "auto"
                  ],
                  "properties": {
                    "subject_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "programme",
                        "asset",
                        "clip"
                      ]
                    },
                    "auto": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "subject_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                  "kind": "asset",
                  "auto": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no programme, video or clip with this id",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID, or the body is not `{\"enabled\": true|false}` (invalid JSON and unknown fields included)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: subtitles are not available, the per-video switch is not available yet (migration 0041) or it could not be saved",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/subtitles/tracks/{id}/cues": {
      "get": {
        "tags": [
          "subtitles"
        ],
        "summary": "A track's cues for the editor (ms on the track timeline: epoch ms for recordings, from 0 for VOD; lc = flagged for review)",
        "description": "**Required scope:** `assets:read`\n\nReturns the track and its cues as stored. With `wall_clock: true` (recordings) cue times are epoch\nmilliseconds; otherwise (assets, clips) they count from 0. Use `track.version` as the `version` of a later\nPUT. Requires scope `assets:read`; platform tenants only.",
        "operationId": "getSubtitleCues",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Track id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cues",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "track",
                    "wall_clock",
                    "cues"
                  ],
                  "properties": {
                    "track": {
                      "$ref": "#/components/schemas/SubtitleTrack"
                    },
                    "wall_clock": {
                      "type": "boolean",
                      "description": "true: cue times are epoch ms (recordings)"
                    },
                    "cues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SubtitleCue"
                      }
                    }
                  }
                },
                "example": {
                  "track": {
                    "id": "01a0e7a8-0c33-7f1a-8b6e-2d4f6a8c0e12",
                    "subject_type": "asset",
                    "subject_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                    "lang": "he",
                    "name": "עברית",
                    "status": "ready",
                    "version": 1,
                    "source": "asr",
                    "model": "asr-he",
                    "word_level": true,
                    "cues": 212,
                    "words": 1980,
                    "low_conf_cues": 3,
                    "rtf": 0.017,
                    "vtt_url": "https://cdn.now14-poc.vustream.net/vod/now14poc/01a0e79f-a725-79e5-80d8-ff1366a12076/subs/he/he.vtt",
                    "created_at": "2026-10-01T11:02:44Z"
                  },
                  "wall_clock": false,
                  "cues": [
                    {
                      "s": 0,
                      "e": 3200,
                      "t": "ערב טוב, ואלה הכותרות"
                    },
                    {
                      "s": 3200,
                      "e": 7850,
                      "t": "הריבית נותרה ללא שינוי\nבהחלטת בנק ישראל",
                      "lc": true
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such subtitle track for this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "`internal_error`: the cues could not be read from object storage",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "`feature_disabled`: subtitles or object storage are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "subtitles"
        ],
        "summary": "Save an edited track as a new version (new object URLs; source becomes edited and is never replaced by a re-run)",
        "description": "**Required scope:** `assets:write`\n\nReplaces the track's cues and writes a new version under new object keys (cues JSON, WebVTT and transcript), so\nno cache serves the old cues and `vtt_url` changes. The track becomes `source: edited`, `status: ready`, with\n`low_conf_cues` 0; an edited track is never replaced by a re-run (POST …/rerun answers 409). Editing the Hebrew\ntrack makes the machine translations re-run. Optimistic lock: `version` must equal the track's current version,\notherwise 409 — reload and edit again. Cues: at most 20000, ordered by start, end after start, non-empty text of\nat most 200 characters and 3 lines; body at most 4 MiB. Requires scope `assets:write`. Audited as `subtitles.edit`.",
        "operationId": "putSubtitleCues",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Track id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version",
                  "cues"
                ],
                "properties": {
                  "version": {
                    "type": "integer",
                    "description": "the track version the edit started from (optimistic lock)"
                  },
                  "cues": {
                    "type": "array",
                    "maxItems": 20000,
                    "items": {
                      "$ref": "#/components/schemas/SubtitleCue"
                    }
                  }
                }
              },
              "example": {
                "version": 1,
                "cues": [
                  {
                    "s": 0,
                    "e": 3200,
                    "t": "ערב טוב, ואלה הכותרות"
                  },
                  {
                    "s": 3200,
                    "e": 7850,
                    "t": "הריבית נותרה ללא שינוי\nבהחלטת בנק ישראל"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the track at its new version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "track"
                  ],
                  "properties": {
                    "track": {
                      "$ref": "#/components/schemas/SubtitleTrack"
                    }
                  }
                },
                "example": {
                  "track": {
                    "id": "01a0e7a8-0c33-7f1a-8b6e-2d4f6a8c0e12",
                    "subject_type": "asset",
                    "subject_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                    "lang": "he",
                    "name": "עברית",
                    "status": "ready",
                    "version": 2,
                    "source": "edited",
                    "model": "asr-he",
                    "word_level": true,
                    "cues": 2,
                    "words": 1980,
                    "low_conf_cues": 0,
                    "rtf": 0.017,
                    "vtt_url": "https://cdn.now14-poc.vustream.net/vod/now14poc/01a0e79f-a725-79e5-80d8-ff1366a12076/subs/he/e2/he.vtt",
                    "created_at": "2026-10-01T11:02:44Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid JSON, has unknown fields or is larger than 4 MiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: no such subtitle track for this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`: the track changed since `version` (the detail names the current version), or changed while saving; reload it",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/conflict",
                  "title": "Conflict",
                  "status": 409,
                  "detail": "the track changed (now version 3); reload it",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: the id is not a UUID, or a cue breaks the rules (the detail names the cue)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "`internal_error`: the old cues could not be read or the new ones not stored",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "`feature_disabled`: subtitles are not available",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}/reencode": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Re-encode a ready asset with another ladder into the next shadow prefix (priority-3 job)",
        "description": "**Required scope:** `assets:write`\n\nRe-encode a ready asset with another ladder into the next shadow prefix (priority-3 job); the master swaps when done\n\nQueues a `reencode` job (bulk priority 3) that encodes the asset's master with `ladder` (trimmed to the\nsource height) into a new shadow prefix `version` (1 + earlier re-encodes); the asset keeps playing its current\nrenditions until the job finishes and the master playlist switches. The new set is encrypted as the asset's\neffective playback policy asks. Async: answers 202 with the job — follow it with `GET /v1/jobs/{id}` or the\nasset's `jobs`. Body max 4 KB. Scope `assets:write`; not for CDN-only tenants. Audited as `asset.reencode`.",
        "operationId": "reencodeAsset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ladder"
                ],
                "additionalProperties": false,
                "properties": {
                  "ladder": {
                    "type": "string",
                    "description": "Ladder profile name (e.g. news-1080p, news-720p, hevc-1080p); unknown = 422"
                  }
                }
              },
              "example": {
                "ladder": "hevc-1080p"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Re-encode job created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job",
                    "version"
                  ],
                  "properties": {
                    "job": {
                      "$ref": "#/components/schemas/Job"
                    },
                    "version": {
                      "type": "integer",
                      "description": "Shadow prefix version the new renditions are written to"
                    }
                  }
                },
                "example": {
                  "job": {
                    "id": "0192c8a0-5000-7000-8000-000000000009",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "type": "reencode",
                    "priority": 3,
                    "status": "queued",
                    "attempts": 0,
                    "max_attempts": 3,
                    "asset_id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                    "created_at": "2026-10-06T09:12:00Z"
                  },
                  "version": 1
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 4 KB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset in this tenant, or it is in the trash",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Asset is not ready or has no master, or the encryption keys for its playback policy cannot be prepared",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`ladder` is not a known ladder profile",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}/ai-video": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "AI video upscale state",
        "description": "**Required scope:** `assets:read`\n\nAI video upscale state: eligibility, plan and estimate (GPU time, wall time), progress, result, before/after pair. status not_eligible | none | pending | ready | failed | cancelled.\n\nRead-only. Without an AI version the answer is `none` with the plan and the estimate the Studio shows before\nstarting, or `not_eligible` with a `reason` (`not_ready`, `large_enough` = already 1080p, `too_small` =\nshort edge below 240, `too_long` = over 60 min, `no_video`, `encrypted` = the asset's policy encrypts it).\nWhile `pending`, `progress` reports the job; when `ready`, `preview_hls` plays the AI set before switching\nand `compare` gives a before/after pair. `active` tells whether the asset plays the AI set. Scope\n`assets:read`; not for CDN-only tenants.",
        "operationId": "getAIVideo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "State",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIVideo"
                },
                "example": {
                  "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                  "status": "ready",
                  "active": false,
                  "plan": {
                    "scale": 2,
                    "model": "RealESRGAN_x2plus",
                    "src_w": 960,
                    "src_h": 540,
                    "out_w": 1920,
                    "out_h": 1080,
                    "fps": 25,
                    "frames": 4464,
                    "chunk_frames": 50,
                    "chunks": 90,
                    "duration_s": 178.56
                  },
                  "estimate": {
                    "frames": 4464,
                    "gpu_s": 232,
                    "wall_s": 726,
                    "upscale_fps": 11.5
                  },
                  "variant": {
                    "id": "01a0f2b0-1c4d-7e2f-8a3b-4c5d6e7f8091",
                    "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                    "status": "ready",
                    "active": false,
                    "out_prefix": "tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/",
                    "temporal_denoise": true,
                    "created_at": "2026-10-03T07:40:00Z",
                    "updated_at": "2026-10-03T07:53:12Z"
                  },
                  "preview_hls": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/master.m3u8",
                  "compare": {
                    "original": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/compare/original.mp4",
                    "ai": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/compare/ai.mp4",
                    "at_s": 59
                  },
                  "limits": {
                    "max_duration_s": 3600,
                    "target_short": 1080
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset in this tenant, or it is in the trash",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI video upscaling is not available on this platform (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Start the AI upscale to a 1080p rendition set next to the originals…",
        "description": "**Required scope:** `assets:write`\n\nStart the AI upscale to a 1080p rendition set next to the originals (Real-ESRGAN on the GPU, no face restoration; a background job the user starts per video, never automatic; at most 60 min of video). Idempotent; a failed or cancelled job resumes from its stored chunks. The ladder switches only with /activate.\n\nPlans the upscale (model x2 or x4 to a 1080-pixel short edge) and queues one GPU job that writes the AI set\nunder `<asset>/ai/` with the tenant's default ladder when it reaches 1080p, else `news-1080p` (trimmed to\nthe output size). The originals are never touched. Calling again while a job is pending returns its state;\nafter `failed`/`cancelled` it resumes from the stored chunks; when the AI set is already `ready` it answers\n200. The body is optional (empty = defaults; max 4 KB). Async: 202 + state — poll `GET.../ai-video` for\n`progress`. Scope `assets:write`; not for CDN-only tenants. Audited as `assets.ai_video.start`.",
        "operationId": "postAIVideo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "temporal_denoise": {
                    "type": "boolean",
                    "default": true,
                    "description": "A light temporal denoise after the upscale (brings the model's flicker back to the source's level)"
                  }
                }
              },
              "example": {
                "temporal_denoise": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The AI set is already ready (nothing queued)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIVideo"
                }
              }
            }
          },
          "202": {
            "description": "Queued (or the pending job's state)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIVideo"
                },
                "example": {
                  "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                  "status": "pending",
                  "active": false,
                  "plan": {
                    "scale": 2,
                    "model": "RealESRGAN_x2plus",
                    "src_w": 960,
                    "src_h": 540,
                    "out_w": 1920,
                    "out_h": 1080,
                    "fps": 25,
                    "frames": 4464,
                    "chunk_frames": 50,
                    "chunks": 90,
                    "duration_s": 178.56
                  },
                  "estimate": {
                    "frames": 4464,
                    "gpu_s": 232,
                    "wall_s": 726,
                    "upscale_fps": 11.5
                  },
                  "progress": {
                    "percent": 0,
                    "job_status": "queued",
                    "attempts": 0
                  },
                  "variant": {
                    "id": "01a0f2b0-1c4d-7e2f-8a3b-4c5d6e7f8091",
                    "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                    "status": "pending",
                    "active": false,
                    "out_prefix": "tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/",
                    "temporal_denoise": true,
                    "job_id": "01a0f2b0-1c4d-7e2f-8a3b-4c5d6e7f8092",
                    "created_at": "2026-10-03T07:40:00Z",
                    "updated_at": "2026-10-03T07:40:00Z"
                  },
                  "limits": {
                    "max_duration_s": 3600,
                    "target_short": 1080
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON or an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset in this tenant, or it is in the trash",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Not eligible (not ready, already 1080p, too small, longer than 60 min, no video, encrypted), or an older 1080p set of the removed CPU method is active (revert first)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI video upscaling is not available on this platform (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}/ai-video/cancel": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Stop the running AI upscale (the chunks done so far are kept; starting again resumes)",
        "description": "**Required scope:** `assets:write`\n\nCancels the pending job, tells the running worker to stop and marks the AI version `cancelled`. When nothing is\npending (ready, failed, already cancelled) it changes nothing and returns the current state. No body. Scope\n`assets:write`; not for CDN-only tenants. Audited as `assets.ai_video.cancel`.",
        "operationId": "cancelAIVideo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "State after the cancel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIVideo"
                },
                "example": {
                  "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                  "status": "cancelled",
                  "active": false,
                  "plan": {
                    "scale": 2,
                    "model": "RealESRGAN_x2plus",
                    "src_w": 960,
                    "src_h": 540,
                    "out_w": 1920,
                    "out_h": 1080,
                    "fps": 25,
                    "frames": 4464,
                    "chunk_frames": 50,
                    "chunks": 90,
                    "duration_s": 178.56
                  },
                  "estimate": {
                    "frames": 4464,
                    "gpu_s": 232,
                    "wall_s": 726,
                    "upscale_fps": 11.5
                  },
                  "variant": {
                    "id": "01a0f2b0-1c4d-7e2f-8a3b-4c5d6e7f8091",
                    "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                    "status": "cancelled",
                    "active": false,
                    "out_prefix": "tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/",
                    "temporal_denoise": true,
                    "job_id": "01a0f2b0-1c4d-7e2f-8a3b-4c5d6e7f8092",
                    "created_at": "2026-10-03T07:40:00Z",
                    "updated_at": "2026-10-03T07:44:10Z"
                  },
                  "limits": {
                    "max_duration_s": 3600,
                    "target_short": 1080
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset, or the asset has no AI upscale",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI video upscaling is not available on this platform (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}/ai-video/activate": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Switch the asset's ladder to the AI 1080p set (the master playlist's video variants; audio and subtitles stay)",
        "description": "**Required scope:** `assets:write`\n\nRewrites the asset's master playlist so its video variants are the AI set's; audio and subtitle renditions stay. The originals are kept for `/revert`. Refused for an asset that is not ready or whose playback policy encrypts it. No body. Scope `assets:write`; not for CDN-only tenants. Audited as `assets.ai_video.activate`.",
        "operationId": "activateAIVideo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Active (`active: true`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIVideo"
                },
                "example": {
                  "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                  "status": "ready",
                  "active": true,
                  "plan": {
                    "scale": 2,
                    "model": "RealESRGAN_x2plus",
                    "src_w": 960,
                    "src_h": 540,
                    "out_w": 1920,
                    "out_h": 1080,
                    "fps": 25,
                    "frames": 4464,
                    "chunk_frames": 50,
                    "chunks": 90,
                    "duration_s": 178.56
                  },
                  "estimate": {
                    "frames": 4464,
                    "gpu_s": 232,
                    "wall_s": 726,
                    "upscale_fps": 11.5
                  },
                  "variant": {
                    "id": "01a0f2b0-1c4d-7e2f-8a3b-4c5d6e7f8091",
                    "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                    "status": "ready",
                    "active": true,
                    "out_prefix": "tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/",
                    "temporal_denoise": true,
                    "created_at": "2026-10-03T07:40:00Z",
                    "updated_at": "2026-10-03T07:53:12Z"
                  },
                  "preview_hls": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/master.m3u8",
                  "compare": {
                    "original": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/compare/original.mp4",
                    "ai": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/compare/ai.mp4",
                    "at_s": 59
                  },
                  "limits": {
                    "max_duration_s": 3600,
                    "target_short": 1080
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset, or the asset has no AI version",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The AI version is not ready or cannot be used (asset not ready, encrypted, audio group or variants not found in the playlists)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI video upscaling is not available on this platform (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/assets/{id}/ai-video/revert": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Back to the original renditions (חזור למקור); the AI set is kept for a later switch",
        "description": "**Required scope:** `assets:write`\n\nRestores the asset's original video variants in its master playlist; the AI set stays in storage for a later `/activate`. No body. Scope `assets:write`; not for CDN-only tenants. Audited as `assets.ai_video.revert`.",
        "operationId": "revertAIVideo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Original in use (`active: false`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AIVideo"
                },
                "example": {
                  "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                  "status": "ready",
                  "active": false,
                  "plan": {
                    "scale": 2,
                    "model": "RealESRGAN_x2plus",
                    "src_w": 960,
                    "src_h": 540,
                    "out_w": 1920,
                    "out_h": 1080,
                    "fps": 25,
                    "frames": 4464,
                    "chunk_frames": 50,
                    "chunks": 90,
                    "duration_s": 178.56
                  },
                  "estimate": {
                    "frames": 4464,
                    "gpu_s": 232,
                    "wall_s": 726,
                    "upscale_fps": 11.5
                  },
                  "variant": {
                    "id": "01a0f2b0-1c4d-7e2f-8a3b-4c5d6e7f8091",
                    "asset_id": "01a0f089-f23c-759c-ad32-bbfef6bc220f",
                    "status": "ready",
                    "active": false,
                    "out_prefix": "tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/",
                    "temporal_denoise": true,
                    "created_at": "2026-10-03T07:40:00Z",
                    "updated_at": "2026-10-03T07:53:12Z"
                  },
                  "preview_hls": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/master.m3u8",
                  "compare": {
                    "original": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/compare/original.mp4",
                    "ai": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/01a0f089-f23c-759c-ad32-bbfef6bc220f/ai/compare/ai.mp4",
                    "at_s": 59
                  },
                  "limits": {
                    "max_duration_s": 3600,
                    "target_short": 1080
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset, or the asset has no AI version",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The original variants are not recorded (re-encode the asset to restore them), or the playlists cannot be rewritten",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "AI video upscaling is not available on this platform (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/connect/token": {
      "post": {
        "tags": [
          "keys"
        ],
        "summary": "Exchange a Connect code for the app's scoped API key (server to server, PKCE)",
        "description": "The app's server (never the browser) exchanges the one-time `code` from the consent redirect, with the PKCE\n`code_verifier` and the identical `redirect_uri`, for a new API key named `<App>: <site host>` with the granted\nscopes. The key is shown once (it appears in Studio → Integrations → API keys, where it can be revoked). A code\nworks once and only within 60 s; a wrong verifier or redirect_uri burns it. No credentials; limited to 10\nexchanges per minute per client IP. Audited as `api_key.create` (`via: connect`).",
        "operationId": "connectToken",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type",
                  "code",
                  "code_verifier",
                  "redirect_uri"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "authorization_code"
                    ]
                  },
                  "code": {
                    "type": "string"
                  },
                  "code_verifier": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9._~-]{43,128}$"
                  },
                  "redirect_uri": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new key (shown once) and its tenant",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "api_key",
                    "key_id",
                    "key_prefix",
                    "key_name",
                    "scopes",
                    "tenant"
                  ],
                  "properties": {
                    "api_key": {
                      "type": "string"
                    },
                    "key_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "key_prefix": {
                      "type": "string"
                    },
                    "key_name": {
                      "type": "string"
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "tenant": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "cdn_hostname": {
                          "type": "string"
                        },
                        "mode": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "api_key": "vs_ab12cd34_…",
                  "key_id": "0192c8a0-6000-7000-8000-0000000000aa",
                  "key_prefix": "ab12cd34",
                  "key_name": "WordPress: news.example.co.il",
                  "scopes": [
                    "assets:read",
                    "assets:write",
                    "clips:read",
                    "channels:read",
                    "ai:read",
                    "webhooks:manage"
                  ],
                  "tenant": {
                    "id": "0192c8a0-6000-7000-8000-000000000001",
                    "slug": "now14poc",
                    "name": "Now 14",
                    "cdn_hostname": "cdn.now14.vustream.net",
                    "mode": "platform"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, or `invalid_grant` (unknown, expired or used code; wrong verifier or redirect_uri)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "grant_type is not authorization_code",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Connected apps are not available yet (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/connect/disconnect": {
      "post": {
        "tags": [
          "keys"
        ],
        "summary": "Disconnect — a connected app revokes its own API key",
        "description": "**Required scope:** none — any valid API key of the tenant.\n\nCalled with the key a Connect created (the WordPress plugin's \"Disconnect\"); revokes that key at once. Keys made\nany other way are refused (revoke them in Studio → Integrations). Audited as `api_key.revoke`.",
        "operationId": "connectDisconnect",
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a Connect key, or a session instead of a key",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Connected apps are not available yet (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/upload-parts/{n}": {
      "put": {
        "tags": [
          "uploads"
        ],
        "summary": "Upload one part straight from a connected site's browser with an upload-only token (CORS)",
        "description": "For connected apps: the app's server starts the upload with `POST /v1/uploads` and `browser_origin` (its\nregistered site origin) and hands only `upload_token` to the browser, which PUTs each 64 MB part here — the\nAPI key never reaches the browser. CORS answers only that origin (`PUT`, `Content-Type`, exposes `ETag`); any\nother Origin is 403. The token allows part uploads of that one upload until it expires (1 h), and stops working\nwhen the key is revoked. Completing the upload (`POST /v1/uploads/{id}/complete`) still needs the key. Same body\nrules as `PUT /v1/uploads/{id}/parts/{n}`; parts may be retried.",
        "operationId": "putBrowserUploadPart",
        "parameters": [
          {
            "name": "n",
            "in": "path",
            "required": true,
            "description": "Part number, 1 – the upload's part count",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            }
          },
          {
            "name": "t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The upload_token returned by POST /v1/uploads"
          },
          {
            "name": "Content-Length",
            "in": "header",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 67108864
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Part stored",
            "headers": {
              "ETag": {
                "description": "The part's ETag",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "part_number",
                    "etag"
                  ],
                  "properties": {
                    "part_number": {
                      "type": "integer"
                    },
                    "etag": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unknown or expired token, or the key was revoked",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The request's Origin is not the connected site's",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Object storage refused the part",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Part number outside the upload, or Content-Length missing / over 64 MB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Browser uploads are not available (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/uploads": {
      "post": {
        "tags": [
          "uploads"
        ],
        "summary": "Start a browser/tool upload",
        "description": "**Required scope:** `assets:write`\n\nStart a browser/tool upload — presigned multipart parts of 64 MB against the object store (versitygw), valid 1 h\n\nOpens a multipart upload in the ingest bucket at `<tenant>/in/<new id>/<filename>` (the filename is reduced to\nletters, digits, `.`, `_` and `-`) and returns one presigned PUT URL per 64 MB part, valid for 1 hour. Upload\neach part to its URL — or, from a browser, through the API with `PUT /v1/uploads/{id}/parts/{n}?key=…` (the\npresigned URLs point at the internal S3 endpoint) — keep each part's ETag, then call\n`POST /v1/uploads/{id}/complete` to register the asset. Max 200 GB (≤ 3200 parts). Body max 4 KB. Scope\n`assets:write` (the `uploads` scope does not open this route); not for CDN-only tenants. Audited as\n`upload.start`.",
        "operationId": "startUpload",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "size_bytes"
                ],
                "additionalProperties": false,
                "properties": {
                  "filename": {
                    "type": "string",
                    "description": "Original file name; only its base name is kept, other characters become `_`"
                  },
                  "size_bytes": {
                    "type": "integer",
                    "format": "int64",
                    "minimum": 1,
                    "maximum": 214748364800,
                    "description": "Total size; decides the number of parts (ceil(size / 64 MB))"
                  },
                  "content_type": {
                    "type": "string",
                    "description": "Stored as the object's Content-Type (not validated)"
                  },
                  "browser_origin": {
                    "type": "string",
                    "description": "Connected apps only: the key's registered site origin (e.g. `https://news.example.co.il`); the answer then also carries `upload_token` and `part_url` for `PUT /v1/upload-parts/{n}` from that site's browser. 403 for any other key or origin."
                  }
                }
              },
              "example": {
                "filename": "evening-news.mov",
                "size_bytes": 209715200,
                "content_type": "video/quicktime"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Upload"
                },
                "example": {
                  "upload_id": "2~Xk9cT0bq3mVZ1n8Rr4Lw7aQe",
                  "key": "now14poc/in/0192c8a0-6000-7000-8000-000000000002/evening-news.mov",
                  "part_size": 67108864,
                  "parts": [
                    {
                      "part_number": 1,
                      "url": "http://s3.vs.internal:7070/ingest/now14poc/in/0192c8a0-6000-7000-8000-000000000002/evening-news.mov?partNumber=1&uploadId=2~Xk9cT0bq3mVZ1n8Rr4Lw7aQe&X-Amz-Signature=…"
                    },
                    {
                      "part_number": 2,
                      "url": "http://s3.vs.internal:7070/ingest/now14poc/in/0192c8a0-6000-7000-8000-000000000002/evening-news.mov?partNumber=2&uploadId=2~Xk9cT0bq3mVZ1n8Rr4Lw7aQe&X-Amz-Signature=…"
                    }
                  ],
                  "expires_at": "2026-10-06T11:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 4 KB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`filename` empty after cleaning, or `size_bytes` outside 1 byte – 200 GB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "Object storage refused the upload or a part could not be presigned",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Object storage is not configured (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/uploads/{id}/parts/{n}": {
      "put": {
        "tags": [
          "uploads"
        ],
        "summary": "Upload one part through the api (same-origin browser path; presigned URLs point at the internal S3 endpoint)",
        "operationId": "putUploadPart",
        "description": "**Required scope:** `assets:write`\n\nDecision D38. Streams the request body (exactly `Content-Length` bytes, at most `part_size` = 64 MB) to object\nstorage as part `n` of the multipart upload and returns its ETag for `POST /v1/uploads/{id}/complete`. 64 MB\nparts stay under Cloudflare's 100 MB request limit; the server allows 15 minutes per part. Cookie sessions need\n`X-VS-Request: 1` like every write. Parts may be retried; the last ETag per part wins. Scope `assets:write`;\nnot for CDN-only tenants.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The upload_id returned by POST /v1/uploads"
          },
          {
            "name": "n",
            "in": "path",
            "required": true,
            "description": "Part number",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            }
          },
          {
            "name": "key",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The key returned by POST /v1/uploads (must be under the tenant's ingest prefix)"
          },
          {
            "name": "Content-Length",
            "in": "header",
            "required": true,
            "description": "Part size in bytes, 1 – 67108864 (chunked bodies without a length are refused)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 67108864
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Part stored",
            "headers": {
              "ETag": {
                "description": "The part's ETag (same as the body's `etag`)",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "part_number",
                    "etag"
                  ],
                  "properties": {
                    "part_number": {
                      "type": "integer"
                    },
                    "etag": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "part_number": 1,
                  "etag": "\"9b2cf535f27731c974343645a3985328\""
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Object storage refused the part (unknown upload id, short body, expired upload)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Part number outside 1–10000, a `key` outside the tenant's ingest prefix, or Content-Length missing / over 64 MB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Object storage is not configured (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/uploads/{id}/complete": {
      "post": {
        "tags": [
          "uploads"
        ],
        "summary": "Complete the multipart upload with the part ETags and register the asset",
        "description": "**Required scope:** `assets:write`\n\nFinishes the multipart upload from the part numbers and ETags, then registers the asset exactly like\n`POST /v1/assets` (source kind `upload`): a `probe` job, status `probing`, the same `external_id`, `title`,\n`ladder`, `publish` and `metadata` rules. Validation runs before the upload is completed, so a 422 leaves the\nupload open for another try within its hour. Body max 1 MB. Scope `assets:write`; not for CDN-only tenants.\nAudited as `asset.create`; emits `asset.status`.",
        "operationId": "completeUpload",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The upload_id returned by POST /v1/uploads"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "key",
                  "parts"
                ],
                "additionalProperties": false,
                "properties": {
                  "key": {
                    "type": "string",
                    "description": "The key returned by POST /v1/uploads"
                  },
                  "parts": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Every uploaded part in order",
                    "items": {
                      "type": "object",
                      "required": [
                        "part_number",
                        "etag"
                      ],
                      "properties": {
                        "part_number": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "etag": {
                          "type": "string",
                          "minLength": 1
                        },
                        "url": {
                          "type": "string",
                          "description": "ignored"
                        }
                      }
                    }
                  },
                  "external_id": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9._:/-]{1,200}$",
                    "description": "CMS id, unique per tenant (409)"
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "ladder": {
                    "type": "string",
                    "description": "Ladder profile; default the tenant's default_ladder"
                  },
                  "publish": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "manual"
                    ],
                    "default": "auto"
                  },
                  "metadata": {
                    "type": "object",
                    "description": "Free-form object, max 16 KB; `metadata.ads` validated as on POST /v1/assets"
                  }
                }
              },
              "example": {
                "key": "now14poc/in/0192c8a0-6000-7000-8000-000000000002/evening-news.mov",
                "parts": [
                  {
                    "part_number": 1,
                    "etag": "\"9b2cf535f27731c974343645a3985328\""
                  },
                  {
                    "part_number": 2,
                    "etag": "\"1f0e3dad99908345f7439f8ffabdffc4\""
                  }
                ],
                "external_id": "cms-48212",
                "title": "Evening news 28.9",
                "publish": "manual"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Asset registered from the upload (status `probing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Asset"
                },
                "example": {
                  "id": "0192c8a0-6100-7000-8000-000000000003",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "external_id": "cms-48212",
                  "title": "Evening news 28.9",
                  "status": "probing",
                  "source": {
                    "kind": "upload",
                    "bucket": "ingest",
                    "key": "now14poc/in/0192c8a0-6000-7000-8000-000000000002/evening-news.mov"
                  },
                  "master_key": null,
                  "ladder": "news-1080p",
                  "probe": null,
                  "duration_ms": null,
                  "error": null,
                  "metadata": {
                    "_publish": "manual"
                  },
                  "published_at": null,
                  "ready_at": null,
                  "created_at": "2026-10-06T10:20:00Z",
                  "updated_at": "2026-10-06T10:20:00Z",
                  "published": false,
                  "renditions": [],
                  "playback": null,
                  "jobs": [
                    {
                      "id": "0192c8a0-6100-7000-8000-000000000004",
                      "type": "probe",
                      "status": "queued",
                      "attempts": 0,
                      "created_at": "2026-10-06T10:20:00Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 1 MB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Object storage could not complete the upload (unknown/expired upload id or bad ETags), or the external_id is taken",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`key` outside the tenant's ingest prefix, no parts / a part without number or etag, or an asset field (`external_id`, `title`, `metadata`, `publish`, `ladder`) is invalid",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Object storage is not configured (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/channels/{id}/ingest/srt-passphrase": {
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Generate or rotate the channel's SRT passphrase (stored sealed; not returned — reveal it with a password)",
        "description": "**Required scope:** `channels:operate`\n\nGenerates a new 32-character passphrase (letters and digits without look-alikes 0/O/1/l/I) for the channel's\nshared SRT ingest, seals it (AES-256-GCM) and replaces any earlier one, including a plaintext `srt_passphrase`\nset through the channel. The response never carries the passphrase: a signed-in user reads it with\n`POST …/srt-passphrase/reveal`. The channel view shows only `srt_passphrase_set` / `srt_passphrase_set_at`.\nEnabling it on the encoder (OME SRT `<Passphrase>`) is a separate, coordinated change — an OME config render\nrestarts every live channel — so `applied_to_encoder` is always false. Needs `channels:operate`.\nAudited as `channel.srt_passphrase.generate` (first time) or `channel.srt_passphrase.rotate`.\nStudio per-feed ingest (`PUT …/ingest`) has its own feed passphrases.",
        "operationId": "rotateSrtPassphrase",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Passphrase stored (sealed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "srt_passphrase_set",
                    "srt_passphrase_set_at",
                    "applied_to_encoder"
                  ],
                  "properties": {
                    "srt_passphrase_set": {
                      "type": "boolean",
                      "const": true
                    },
                    "srt_passphrase_set_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "applied_to_encoder": {
                      "type": "boolean",
                      "description": "always false: the encoder change is a separate operator step"
                    }
                  }
                },
                "example": {
                  "srt_passphrase_set": true,
                  "srt_passphrase_set_at": "2026-10-06T08:14:02Z",
                  "applied_to_encoder": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:operate"
      }
    },
    "/v1/channels/{id}/sla": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "Availability and events of a channel over a period — minutes down by cause and the alerts and feed events",
        "description": "**Required scope:** `channels:read`\n\nAvailability per minute from the platform's own data: the recorder's runs (the audio rendition = every recorded\nminute), the slate on/off and failover events, and the channel's alerts. A minute is down when, for at least\n30 s of it, the channel was not recorded, the slate was on air, or a `feed_down` alert was firing; the cause is\n`source` when the slate or a `feed_down` alert was active in that minute, otherwise `platform`. Minutes before\nthe channel's first recording in the period are left out. The `rule` field carries the rule in force (an interim\nrule until the external ISP probes of the readiness gates exist). `events` merges the channel's alerts (with\ntheir end) and the feed events, oldest first. Defaults: the last 7 days; at most 31 days. Studio shows it under\nthe channel → \"זמינות ואירועים\"; `viewstream admin soak-report` prints the 72-hour soak report from it.\nNeeds `channels:read`.",
        "operationId": "getChannelSLA",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start, RFC 3339 or epoch ms (default: 7 days before `to`)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "End, RFC 3339 or epoch ms (default and maximum: now)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelSLA"
                },
                "example": {
                  "channel_id": "01a0e4de-b244-7256-830a-9cfb9e89333b",
                  "from": "2026-10-04T12:00:00Z",
                  "to": "2026-10-07T12:00:00Z",
                  "rule": "A minute is down when, for at least 30 s of it, …",
                  "minutes": 4320,
                  "down_minutes": 6,
                  "down_minutes_source": 6,
                  "down_minutes_platform": 0,
                  "availability_pct": 99.861,
                  "periods": [
                    {
                      "start": "2026-10-05T09:12:00Z",
                      "end": "2026-10-05T09:18:00Z",
                      "minutes": 6,
                      "cause": "source"
                    }
                  ],
                  "events": [
                    {
                      "at": "2026-10-05T09:12:04Z",
                      "kind": "slate_on",
                      "source": "feed",
                      "detail": "enc-1"
                    },
                    {
                      "at": "2026-10-05T09:12:30Z",
                      "end": "2026-10-05T09:18:10Z",
                      "kind": "feed_down",
                      "source": "alert",
                      "severity": "critical",
                      "title_he": "השידור של הערוץ נפל",
                      "title_en": "Channel feed is down"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error`: from / to unreadable, or not before / more than 31 days apart",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: reports are not available on this deployment",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/ad-settings": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "Ad cue source and server-side ad insertion (SSAI) settings of a channel (video-ads phase 2), with the cue…",
        "description": "**Required scope:** `channels:read`\n\nAd cue source and server-side ad insertion (SSAI) settings of a channel (video-ads phase 2), with the cue poller's last state\n\nReturns the channel's ad cue source, the offset that maps cue times onto the recording timeline, the SSAI switch,\nthe VAST tag and the cue poller's last poll (`polled_at`, `poll_error`, `poll_state`). A channel that was never\nconfigured answers the defaults (`cue_source: none`, SSAI off, `vast_timeout_ms: 2000`). Needs `channels:read`.",
        "operationId": "getChannelAdSettings",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The settings (defaults when never set)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelAdSettings"
                },
                "example": {
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "cue_source": "redge_dai",
                  "cue_url": "https://dai.example-broadcaster.tv/live/ch10/dai.livx",
                  "cue_offset_ms": null,
                  "effective_offset_ms": 8000,
                  "ssai_enabled": true,
                  "vast_tag": "https://ads.example.com/vast?ch=[CHANNEL]&dur=[BREAKDURATION]&cb=[CACHEBUSTING]",
                  "vast_timeout_ms": 2000,
                  "polled_at": "2026-10-06T08:20:04Z",
                  "poll_error": null,
                  "poll_state": {
                    "period_id": "p-1728202800",
                    "period_start": "2026-10-06T08:20:00Z",
                    "in_break": false,
                    "breaks": 14
                  },
                  "updated_by": "user:editor@tv10poc.example",
                  "updated_at": "2026-10-05T16:02:11Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ad breaks are not available yet (`feature_disabled`, migration 0063 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "live"
        ],
        "summary": "Set the channel's ad cue source and SSAI switch (audited channel.ad_settings)",
        "description": "**Required scope:** `channels:write`\n\nReplaces the whole settings object (omitted fields take their defaults). `cue_source: redge_dai` needs `cue_url`\n(the broadcaster's Redge DAI DASH manifest, …/dai.livx; http(s), public addresses only — checked against the SSRF\nguard): the orchestrator polls it every 4 s when AD_CUES_ENABLED and records each SCTE-35 cue-out Period as a\nbreak. A new cue URL or source resets the poller state. `cue_offset_ms` maps the cue clock onto the recording\ntimeline (null = the channel's air delay). `ssai_enabled` needs `vast_tag` (http(s); macros [CACHEBUSTING],\n[TIMESTAMP], [BREAKDURATION], [SESSION_ID], [CHANNEL], [BREAK_ID]); viewers get server-side ads only from players\nthat ask with `?ssai=1` (Players → Ads → server-side ads) and only while the platform has SSAI_ENABLED. `scte35`\nis reserved (no feed carries SCTE-35 today). Empty strings clear `cue_url` / `vast_tag`. Body at most 16 KiB.\nNeeds `channels:write`; audited as `channel.ad_settings`.\n\nv3 (migration 0082; each field optional — absent keeps the stored value): `decisioning` picks the ad\ndecisioning adapter — `vast` (the generic `vast_tag`, default), `gam` (Google Ad Manager: `decisioning_params`\n`network_code`, `ad_unit`, optional `size`, `max_ads`, `content_source`, `description_url`), `freewheel`\n(`server` origin, `network_id`, `profile`, `site_section`, optional `video_asset_id`) or `adocean`\n(`emitter` origin, `placement_id`); `kv_<key>` params add static key-values. Requests carry the break length,\nconsent (`?consent=1|0` from the player; unknown = non-personalised), country (CDN header), device class,\nthe programme id and — with `contextual_kv` (default true) — its AI topics as `vs_topic`. New macros for\n`vast_tag`: [CONTENT_ID], [CONSENT], [GDPR], [COUNTRY], [DEVICE_TYPE], [KV]. Non-VAST adapters time out at\n1.5 s (or `vast_timeout_ms` if lower). `fallback_tag` (generic VAST, macros allowed) answers when the primary\nerrors, times out or does not fill. `freq_cap_per_hour` (1–100; 0 or null = none) caps stitched ads per viewer\nsession per hour. Adapter origins and the fallback pass the SSRF guard. Server-side ads also need the tenant's\nreplacement rights (`/v1/ads/rights`).",
        "operationId": "putChannelAdSettings",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cue_source": {
                    "type": "string",
                    "enum": [
                      "none",
                      "redge_dai",
                      "scte35",
                      "manual"
                    ],
                    "default": "none"
                  },
                  "cue_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 1024,
                    "description": "http(s) URL; required for redge_dai"
                  },
                  "cue_offset_ms": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": -600000,
                    "maximum": 600000,
                    "description": "null = the channel's air delay"
                  },
                  "ssai_enabled": {
                    "type": "boolean",
                    "default": false
                  },
                  "vast_tag": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 4096,
                    "description": "http(s) URL with macros; required when ssai_enabled"
                  },
                  "vast_timeout_ms": {
                    "type": "integer",
                    "minimum": 300,
                    "maximum": 10000,
                    "default": 2000
                  },
                  "decisioning": {
                    "type": "string",
                    "enum": [
                      "vast",
                      "gam",
                      "freewheel",
                      "adocean"
                    ],
                    "description": "v3; absent = unchanged"
                  },
                  "decisioning_params": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "v3; the adapter's parameters (see above); absent = unchanged"
                  },
                  "fallback_tag": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 4096,
                    "description": "v3; generic VAST URL; \"\" clears it"
                  },
                  "freq_cap_per_hour": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 100,
                    "description": "v3; 0 or null = no cap"
                  },
                  "contextual_kv": {
                    "type": "boolean",
                    "description": "v3; send the programme's AI topics as vs_topic"
                  }
                }
              },
              "example": {
                "cue_source": "redge_dai",
                "cue_url": "https://dai.example-broadcaster.tv/live/ch10/dai.livx",
                "cue_offset_ms": null,
                "ssai_enabled": true,
                "vast_tag": "https://ads.example.com/vast?ch=[CHANNEL]&dur=[BREAKDURATION]&cb=[CACHEBUSTING]",
                "vast_timeout_ms": 2000
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the settings as stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelAdSettings"
                },
                "example": {
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "cue_source": "redge_dai",
                  "cue_url": "https://dai.example-broadcaster.tv/live/ch10/dai.livx",
                  "cue_offset_ms": null,
                  "effective_offset_ms": 8000,
                  "ssai_enabled": true,
                  "vast_tag": "https://ads.example.com/vast?ch=[CHANNEL]&dur=[BREAKDURATION]&cb=[CACHEBUSTING]",
                  "vast_timeout_ms": 2000,
                  "polled_at": "2026-10-06T08:20:04Z",
                  "poll_error": null,
                  "poll_state": {
                    "period_id": "p-1728202800",
                    "period_start": "2026-10-06T08:20:00Z",
                    "in_break": false,
                    "breaks": 14
                  },
                  "updated_by": "user:editor@tv10poc.example",
                  "updated_at": "2026-10-05T16:02:11Z"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid settings; `errors[]` names cue_source, cue_url (missing for redge_dai, not http(s), too long, not a public address), cue_offset_ms, vast_tag (SSAI without a tag, not http(s), too long) or vast_timeout_ms",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ad breaks are not available yet (`feature_disabled`, migration 0063 missing; or a v3 field before migration 0082)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/ads/rights": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "The tenant's broadcast ad replacement rights declaration",
        "description": "**Required scope:** `channels:read` or `tenant:settings`\n\nWhether the tenant declared that it holds the online rights to replace its broadcast's ad breaks for online\nviewers (design V9, legal). Server-side ad insertion — live (`/m/ssai/live/…`) and at ad breaks on catch-up and\nstart-over (`?ssai=1`) — replaces the broadcast's breaks, so once the platform has this flag (migration 0082) a\nchannel's `ssai_enabled` takes effect only for a tenant with `online_rights: true`. Tenants that already had\nserver-side ads on were grandfathered by the migration. Never declared: `online_rights: false` and nulls.\nNeeds `channels:read` or `tenant:settings`.",
        "operationId": "getAdRights",
        "responses": {
          "200": {
            "description": "The declaration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdRights"
                },
                "example": {
                  "online_rights": true,
                  "note": "Online simulcast rights incl. ad replacement, agreement of 2026-09-01",
                  "declared_by": "user:admin@tv10poc.example",
                  "declared_at": "2026-10-08T07:12:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0082 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read or tenant:settings"
      },
      "put": {
        "tags": [
          "live"
        ],
        "summary": "Declare (or withdraw) the tenant's broadcast ad replacement rights (audited tenant.ad_rights)",
        "description": "**Required scope:** `tenant:settings`\n\nSets `online_rights` and an optional `note` (≤ 1000 characters — e.g. the agreement it rests on; \"\" clears it).\nRecords who and when. `false` stops server-side ad replacement on every channel of the tenant within 30 s\n(the channels keep their settings). Needs `tenant:settings`; audited as `tenant.ad_rights`.",
        "operationId": "putAdRights",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "online_rights"
                ],
                "properties": {
                  "online_rights": {
                    "type": "boolean"
                  },
                  "note": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 1000
                  }
                }
              },
              "example": {
                "online_rights": true,
                "note": "Online simulcast rights incl. ad replacement, agreement of 2026-09-01"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdRights"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0082 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/ads/vod-settings": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "The tenant's library (VOD) ad settings — on/off, delivery, ad server and default ad positions",
        "description": "**Required scope:** `assets:read` or `tenant:settings`\n\nLibrary ads put ads into the tenant's on-demand videos at ad positions: a pre-roll, mid-rolls at chapter starts\n(`metadata.chapters`), at cue points (`metadata.ads.cues`) and/or every N seconds (at least `min_spacing_s` apart,\nnone in the first `guard_start_s` / last `guard_end_s` seconds), a post-roll, each with a pod size (`max_ads`,\n`max_s`). Positions snap to the stored renditions' segment boundaries (every segment starts at a keyframe).\n`delivery: ssai` stitches the ads into `/m/ssai/vod/<prefix>/<asset>/master.m3u8` (Player v2 switches to it by\nitself); `client` serves `/m/ssai/vod/<prefix>/<asset>/vmap.xml` to the player's VAST/VMAP engine. The ad server\nfields are those of a channel's ad settings (adapters vast | gam | freewheel | adocean, fallback tag, per-session\ncap, contextual key-values `vs_content=vod`, `vs_pos=pre|mid|post`). Off by default; like broadcast ad replacement,\nnothing plays until the tenant declared its ad rights (`/v1/ads/rights`, `online_rights` echoed here) and the\nplatform runs server-side ads (`stitching`). The clean `/vod/` playlists never change. Needs `assets:read` or\n`tenant:settings`.",
        "operationId": "getVODAdSettings",
        "responses": {
          "200": {
            "description": "The settings (defaults, `enabled: false`, when never saved)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VODAdSettings"
                },
                "example": {
                  "enabled": true,
                  "delivery": "ssai",
                  "decisioning": "vast",
                  "decisioning_params": {},
                  "vast_tag": "https://ads.example.com/vast?pos=[KV]&cb=[CACHEBUSTING]",
                  "fallback_tag": null,
                  "vast_timeout_ms": 1500,
                  "freq_cap_per_hour": 6,
                  "contextual_kv": true,
                  "positions": {
                    "pre_roll": true,
                    "mid_chapters": true,
                    "mid_cues": true,
                    "mid_every_s": 600,
                    "min_spacing_s": 300,
                    "guard_start_s": 60,
                    "guard_end_s": 60,
                    "post_roll": false,
                    "pre_pod": {
                      "max_ads": 2,
                      "max_s": 30
                    },
                    "mid_pod": {
                      "max_ads": 3,
                      "max_s": 90
                    },
                    "post_pod": {
                      "max_ads": 2,
                      "max_s": 30
                    }
                  },
                  "updated_by": "user:admin@tv10poc.example",
                  "updated_at": "2026-10-08T09:00:00Z",
                  "online_rights": true,
                  "stitching": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0093 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read or tenant:settings"
      },
      "put": {
        "tags": [
          "assets"
        ],
        "summary": "Set the tenant's library ad settings (audited tenant.vod_ad_settings)",
        "description": "**Required scope:** `tenant:settings`\n\nReplaces the settings. `vast_tag` is required for `enabled` with the generic VAST adapter; the other adapters need\ntheir `decisioning_params` (as for a channel). Tags and ad-server addresses must be public http(s) addresses (the\nserver fetches them). `positions` absent = the default positions. Changes reach playback within 30 s. Needs\n`tenant:settings`; audited as `tenant.vod_ad_settings`.",
        "operationId": "putVODAdSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "default": false
                  },
                  "delivery": {
                    "type": "string",
                    "enum": [
                      "ssai",
                      "client"
                    ],
                    "default": "ssai"
                  },
                  "decisioning": {
                    "type": "string",
                    "enum": [
                      "vast",
                      "gam",
                      "freewheel",
                      "adocean"
                    ],
                    "default": "vast"
                  },
                  "decisioning_params": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  },
                  "vast_tag": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 4096
                  },
                  "fallback_tag": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 4096
                  },
                  "vast_timeout_ms": {
                    "type": "integer",
                    "minimum": 300,
                    "maximum": 10000,
                    "default": 1500
                  },
                  "freq_cap_per_hour": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 100,
                    "description": "0 or null = no cap"
                  },
                  "contextual_kv": {
                    "type": "boolean",
                    "default": true
                  },
                  "positions": {
                    "$ref": "#/components/schemas/VODAdPositions"
                  }
                }
              },
              "example": {
                "enabled": true,
                "delivery": "ssai",
                "vast_tag": "https://ads.example.com/vast?cb=[CACHEBUSTING]&kv=[KV]",
                "positions": {
                  "pre_roll": true,
                  "mid_chapters": true,
                  "mid_cues": true,
                  "mid_every_s": 600,
                  "min_spacing_s": 300,
                  "guard_start_s": 60,
                  "guard_end_s": 60,
                  "post_roll": false,
                  "pre_pod": {
                    "max_ads": 2,
                    "max_s": 30
                  },
                  "mid_pod": {
                    "max_ads": 3,
                    "max_s": 90
                  },
                  "post_pod": {
                    "max_ads": 2,
                    "max_s": 30
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VODAdSettings"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0093 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/assets/{id}/ad-positions": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "A video's ad positions — its override, the effective positions, chapters and the planned ad breaks",
        "description": "**Required scope:** `assets:read`\n\nThe video's own override (`null` = inherit from its series, then the tenant default), the positions that apply\nand where they come from (`asset`, `collection`, `tenant`), its chapters and cue points, and the planned breaks\n(`slots`, milliseconds on the content timeline, snapped to the ladder's segment grid). `blocked` says why no ad\nwould play now (`disabled`, `no_rights`, `no_ad_server`, `not_ready`, `off`, `platform_off`). `ssai_url` and\n`vmap_url` are the playback URLs on the tenant's CDN host. Needs `assets:read`.",
        "operationId": "getAssetAdPositions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The video's ad positions",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetAdPositions"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0093 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "assets"
        ],
        "summary": "Set a video's own ad positions, or no ads on it (audited asset.ad_positions)",
        "description": "**Required scope:** `assets:write`\n\n`mode: custom` with `positions` (absent = the defaults) overrides the series and tenant default for this video;\n`mode: off` plays it without library ads. Answers the same view as GET. Needs `assets:write`.",
        "operationId": "putAssetAdPositions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VODAdOverrideInput"
              },
              "example": {
                "mode": "custom",
                "positions": {
                  "pre_roll": true,
                  "mid_chapters": true,
                  "mid_cues": false,
                  "mid_every_s": 0,
                  "min_spacing_s": 240,
                  "guard_start_s": 60,
                  "guard_end_s": 60,
                  "post_roll": true,
                  "pre_pod": {
                    "max_ads": 1,
                    "max_s": 15
                  },
                  "mid_pod": {
                    "max_ads": 2,
                    "max_s": 60
                  },
                  "post_pod": {
                    "max_ads": 1,
                    "max_s": 30
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the video's ad positions",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetAdPositions"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0093 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      },
      "delete": {
        "tags": [
          "assets"
        ],
        "summary": "Remove a video's own ad positions (back to its series or the tenant default)",
        "description": "**Required scope:** `assets:write`\n\nRemoves the override; answers the video's ad positions. Needs `assets:write`; audited as `asset.ad_positions`.",
        "operationId": "deleteAssetAdPositions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The video's ad positions after the reset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetAdPositions"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0093 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/collections/{id}/ad-positions": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "A series' (section's) own ad positions",
        "description": "**Required scope:** `assets:read`\n\n`override: null` = the series inherits the tenant default. A video without its own override takes the positions\nof its nearest series that has one (sub-section before its parent). Needs `assets:read`.",
        "operationId": "getCollectionAdPositions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The series' override",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "override": {
                      "$ref": "#/components/schemas/VODAdOverride"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such section in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0093 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "put": {
        "tags": [
          "assets"
        ],
        "summary": "Set a series' ad positions, or no ads in it (audited collection.ad_positions)",
        "description": "**Required scope:** `assets:write`\n\n`mode: custom` with `positions`, or `mode: off`. Needs `assets:write`.",
        "operationId": "putCollectionAdPositions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VODAdOverrideInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "override": {
                      "$ref": "#/components/schemas/VODAdOverride"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such section in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0093 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      },
      "delete": {
        "tags": [
          "assets"
        ],
        "summary": "Remove a series' ad positions (back to the tenant default)",
        "description": "**Required scope:** `assets:write`\n\nNeeds `assets:write`; audited as `collection.ad_positions`.",
        "operationId": "deleteCollectionAdPositions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed (`override: null`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "override": {
                      "$ref": "#/components/schemas/VODAdOverride"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such section in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0093 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/channels/{id}/ad-breaks": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "The channel's ad breaks overlapping [from, to) on the recording timeline, oldest first…",
        "description": "**Required scope:** `channels:read`\n\nThe channel's ad breaks overlapping [from, to) on the recording timeline, oldest first (default the last 24 h; at most 31 days)\n\nLists breaks of every source (cue poller, editors, detection) that overlap the window. Without parameters the\nwindow is now − 24 h … now + 1 h. Open breaks (cue-out seen, cue-in not yet) have `duration_ms: null`. Not paged.\nNeeds `channels:read`.",
        "operationId": "listChannelAdBreaks",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Window start (RFC 3339); default now − 24 h",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Window end (RFC 3339), after `from` and at most 31 days later; default now + 1 h",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The window actually used and its breaks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "from",
                    "to",
                    "breaks"
                  ],
                  "properties": {
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "breaks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdBreak"
                      }
                    }
                  }
                },
                "example": {
                  "from": "2026-10-06T00:00:00Z",
                  "to": "2026-10-07T00:00:00Z",
                  "breaks": [
                    {
                      "id": "01a10e44-91c2-7f0b-8a3d-6c2e9b7f4a15",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "start_at": "2026-10-06T19:42:10Z",
                      "duration_ms": 180000,
                      "end_at": "2026-10-06T19:45:10Z",
                      "source": "manual",
                      "source_ref": null,
                      "note": "Main news — first break",
                      "created_by": "user:editor@tv10poc.example",
                      "created_at": "2026-10-06T19:50:31Z",
                      "updated_by": null,
                      "updated_at": "2026-10-06T19:50:31Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`from` / `to` not RFC 3339, `to` not after `from`, or the window is longer than 31 days",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ad breaks are not available yet (`feature_disabled`, migration 0063 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Add a manual ad break (audited channel.ad_break.create); 409 when it overlaps another break of the channel",
        "description": "**Required scope:** `channels:write`\n\nAdds an editor's break (`source: manual`) on the recording timeline. `start_at` and `duration_ms` are required;\n`start_at` must lie within the last 120 days or the next 24 hours, `duration_ms` 1 s … 30 min. A break that\noverlaps another break of the channel (an open break counts as 60 s) is refused with 409. Cue breaks the poller\nfinds later never overwrite a manual break. Body at most 4 KiB. Needs `channels:write`.",
        "operationId": "createChannelAdBreak",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdBreakInput"
              },
              "example": {
                "start_at": "2026-10-06T19:42:10Z",
                "duration_ms": 180000,
                "note": "Main news — first break"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdBreak"
                },
                "example": {
                  "id": "01a10e44-91c2-7f0b-8a3d-6c2e9b7f4a15",
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "start_at": "2026-10-06T19:42:10Z",
                  "duration_ms": 180000,
                  "end_at": "2026-10-06T19:45:10Z",
                  "source": "manual",
                  "source_ref": null,
                  "note": "Main news — first break",
                  "created_by": "user:editor@tv10poc.example",
                  "created_at": "2026-10-06T19:50:31Z",
                  "updated_by": null,
                  "updated_at": "2026-10-06T19:50:31Z"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The break overlaps another break of the channel (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`start_at` missing or out of range, `duration_ms` outside 1000..1800000, or `note` longer than 500 characters",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ad breaks are not available yet (`feature_disabled`, migration 0063 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/ad-breaks/{bid}": {
      "patch": {
        "tags": [
          "live"
        ],
        "summary": "Move, resize or annotate a break of any source (audited channel.ad_break.update)",
        "description": "**Required scope:** `channels:write`\n\nMove, resize or annotate a break of any source (audited channel.ad_break.update); an edited cue break is never overwritten by the cue poller\n\nPartial update: fields left out keep their value; `note: \"\"` clears the note. The result is validated like a\nnew break, with the range check centred on the break's current start (an old break stays editable) — an open\ncue break needs `duration_ms`. 409 when the new span overlaps another break. Body at most 4 KiB.\nNeeds `channels:write`.",
        "operationId": "updateChannelAdBreak",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "bid",
            "in": "path",
            "required": true,
            "description": "Ad break id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdBreakInput"
              },
              "example": {
                "duration_ms": 150000,
                "note": ""
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated break",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdBreak"
                },
                "example": {
                  "id": "01a10e44-91c2-7f0b-8a3d-6c2e9b7f4a15",
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "start_at": "2026-10-06T19:42:10Z",
                  "duration_ms": 150000,
                  "end_at": "2026-10-06T19:44:40Z",
                  "source": "redge_dai",
                  "source_ref": "p-1728243730",
                  "note": null,
                  "created_by": null,
                  "created_at": "2026-10-06T19:42:14Z",
                  "updated_by": "user:editor@tv10poc.example",
                  "updated_at": "2026-10-06T19:58:02Z"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel, or no such ad break on it",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The new span overlaps another break of the channel (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`start_at` out of range, `duration_ms` missing or outside 1000..1800000, or `note` longer than 500 characters",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ad breaks are not available yet (`feature_disabled`, migration 0063 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      },
      "delete": {
        "tags": [
          "live"
        ],
        "summary": "Delete a break (audited channel.ad_break.delete)",
        "description": "**Required scope:** `channels:write`\n\nRemoves a break of any source (manual, cue or detected). Needs `channels:write`.",
        "operationId": "deleteChannelAdBreak",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "bid",
            "in": "path",
            "required": true,
            "description": "Ad break id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel, or no such ad break on it",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ad breaks are not available yet (`feature_disabled`, migration 0063 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/recording": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "Recording mode of a channel — single, or two recorders (legs) merged into one recording",
        "description": "**Required scope:** `channels:read`\n\nReturns the mode, its legs and revision, the storage estimate per day for this mode and for single mode, whether\nthe platform allows the dual modes, and the recorder hosts a leg can name. A channel never configured is\n`single` at revision 0. Needs `channels:read`.",
        "operationId": "getChannelRecording",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The recording mode",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelRecording"
                },
                "example": {
                  "mode": "dual_same_source",
                  "legs": [
                    {
                      "leg": 1,
                      "encoder": "enc-1"
                    },
                    {
                      "leg": 2,
                      "encoder": "enc-2"
                    }
                  ],
                  "revision": 3,
                  "updated_by": "user:ops@now14poc.example",
                  "updated_at": "2026-10-04T10:12:45Z",
                  "warnings": [],
                  "storage_bytes_per_day": 129600000000,
                  "single_storage_bytes_per_day": 64800000000,
                  "dual_modes_enabled": true,
                  "encoders": [
                    "enc-1",
                    "enc-2"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "live"
        ],
        "summary": "Set the recording mode",
        "description": "**Required scope:** `channels:write`\n\nSet the recording mode; the legs' encoder hosts pick the channel up within a minute (audited channel.recording.update)\n\nStores a new revision, points the channel's encoders at the legs and invalidates the manifest cache; the\nrecorders pick the change up within a minute. Dual modes are refused with 403 `feature_disabled` unless the\nplatform enables them (RECORDING_REDUNDANCY, off by default) and need exactly leg 1 and leg 2, each with a\nrecorder host name. `dual_split_source` needs two different hosts and the channel on the ingest agent with feed\nB applied; `dual_same_source` on one host is accepted with a warning (it protects against a recorder failure\nonly). Going back to `single` keeps leg 1 (or the current primary) recording and stops leg 2. Send `revision`\nfrom GET to get 409 instead of overwriting a concurrent change. Body at most 16 KiB. Needs `channels:write`;\naudited as `channel.recording.update`.",
        "operationId": "putChannelRecording",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mode"
                ],
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "single",
                      "dual_same_source",
                      "dual_split_source"
                    ]
                  },
                  "legs": {
                    "type": "array",
                    "minItems": 2,
                    "maxItems": 2,
                    "items": {
                      "$ref": "#/components/schemas/RecordingLeg"
                    },
                    "description": "dual modes: exactly leg 1 and leg 2; ignored for single"
                  },
                  "revision": {
                    "type": "integer",
                    "description": "the revision the change is based on; 409 when it moved"
                  }
                }
              },
              "example": {
                "mode": "dual_same_source",
                "legs": [
                  {
                    "leg": 1,
                    "encoder": "enc-1"
                  },
                  {
                    "leg": 2,
                    "encoder": "enc-2"
                  }
                ],
                "revision": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the new mode with any warnings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelRecording"
                },
                "example": {
                  "mode": "dual_same_source",
                  "legs": [
                    {
                      "leg": 1,
                      "encoder": "enc-1"
                    },
                    {
                      "leg": 2,
                      "encoder": "enc-2"
                    }
                  ],
                  "revision": 3,
                  "updated_by": "user:ops@now14poc.example",
                  "updated_at": "2026-10-04T10:12:45Z",
                  "warnings": [],
                  "storage_bytes_per_day": 129600000000,
                  "single_storage_bytes_per_day": 64800000000,
                  "dual_modes_enabled": true,
                  "encoders": [
                    "enc-1",
                    "enc-2"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing, a CDN-only tenant, or a dual mode while the platform has them off (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`revision` does not match the current one, or the settings changed meanwhile (`conflict`) — reload",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Unknown mode; legs not exactly 1 and 2 with valid host names; split-source on one host; or split-source without feed B applied on the ingest agent",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/recording/status": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "Recording status per leg (recording | behind | down), the serving leg and the overall status…",
        "description": "**Required scope:** `channels:read`\n\nRecording status per leg (recording | behind | down), the serving leg and the overall status (ok | redundancy_lost | down)\n\nComputed live from each leg's newest indexed segment of the audio rendition in the last 10 minutes: up to 60 s\nold = recording, up to 180 s = behind, older or none = down. A single-mode channel has one leg (its primary\nencoder). Needs `channels:read`.",
        "operationId": "getChannelRecordingStatus",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingStatus"
                },
                "example": {
                  "mode": "dual_same_source",
                  "status": "redundancy_lost",
                  "serving_leg": 1,
                  "legs": [
                    {
                      "leg": 1,
                      "encoder": "enc-1",
                      "state": "recording",
                      "last_segment_at": "2026-10-06T08:31:54Z"
                    },
                    {
                      "leg": 2,
                      "encoder": "enc-2",
                      "state": "down",
                      "last_segment_at": "2026-10-06T08:24:06Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/recording/status/history": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "Recording status transitions of a dual channel (overall status, serving leg or a leg's state changed)…",
        "description": "**Required scope:** `channels:read`\n\nRecording status transitions of a dual channel (overall status, serving leg or a leg's state changed), newest first, at most 50\n\nThe stored transitions of the channel's recording status. Not paged (the newest 50).\nEmpty for channels recorded by one recorder. Needs `channels:read`.",
        "operationId": "getChannelRecordingStatusHistory",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transitions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "maxItems": 50,
                      "items": {
                        "$ref": "#/components/schemas/RecordingStatusChange"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "at": "2026-10-06T08:25:06Z",
                      "mode": "dual_same_source",
                      "status": "redundancy_lost",
                      "serving_leg": 1,
                      "legs": [
                        {
                          "leg": 1,
                          "encoder": "enc-1",
                          "state": "recording",
                          "last_segment_at": "2026-10-06T08:25:02Z"
                        },
                        {
                          "leg": 2,
                          "encoder": "enc-2",
                          "state": "behind",
                          "last_segment_at": "2026-10-06T08:24:06Z"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/recording/revisions": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "Recording mode history of a channel, newest first (at most 20)",
        "description": "**Required scope:** `channels:read`\n\nEvery stored recording mode change (`PUT …/recording`), newest first; not paged. Needs `channels:read`.",
        "operationId": "getChannelRecordingRevisions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revisions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "maxItems": 20,
                      "items": {
                        "$ref": "#/components/schemas/RecordingRevision"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "revision": 3,
                      "mode": "dual_same_source",
                      "legs": [
                        {
                          "leg": 1,
                          "encoder": "enc-1"
                        },
                        {
                          "leg": 2,
                          "encoder": "enc-2"
                        }
                      ],
                      "updated_by": "user:ops@now14poc.example",
                      "at": "2026-10-04T10:12:45Z"
                    },
                    {
                      "revision": 2,
                      "mode": "single",
                      "legs": [],
                      "updated_by": "key:a1b2c3d4",
                      "at": "2026-10-01T07:40:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/ingest": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "Ingest settings of a channel — feeds A/B, failover policy, push endpoints, agent status, allowed IPs, probes",
        "description": "**Required scope:** `channels:read`\n\nThe full Studio ingest view: the latest (draft) and the applied revision, apply state, the operator control,\npush endpoints, the encoder agent's last report (`status_stale` after 30 s without one), encoder source\naddresses, connectivity checks, the operator relay the channel may still run from and the latest migration\nrequest. Secrets are never returned. A channel without saved settings answers `configured: false`.\nNeeds `channels:read`.",
        "operationId": "getChannelIngest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The ingest view",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelIngest"
                },
                "example": {
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "encoder": "enc-1",
                  "configured": true,
                  "managed": true,
                  "desired_rev": 7,
                  "applied_rev": 7,
                  "apply_state": "applied",
                  "apply_error": null,
                  "apply_at": "2026-10-06T08:01:40Z",
                  "control": "auto",
                  "latest": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "applied": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "endpoints": {
                    "a": {
                      "mode": "push_srt",
                      "url": "srt://ingest-poc.vustream.net:10104?latency=200000&streamid=tv10poc%2Fmain%2Fa",
                      "host": "ingest-poc.vustream.net",
                      "port": 10104,
                      "proto": "udp",
                      "stream_id": "tv10poc/main/a"
                    }
                  },
                  "status": {
                    "active": "a",
                    "running_rev": 7,
                    "feeds": {
                      "a": {
                        "state": "up",
                        "since": "2026-10-06T08:01:52Z",
                        "bytes_per_s": 810359,
                        "restarts": 0,
                        "listen_port": 10104
                      },
                      "b": {
                        "state": "up",
                        "since": "2026-10-06T08:01:49Z",
                        "bytes_per_s": 612004,
                        "restarts": 0
                      }
                    },
                    "extra": {
                      "control": "auto",
                      "applying": false,
                      "firewall": "ok",
                      "playlist_fresh": true
                    }
                  },
                  "status_at": "2026-10-06T08:32:10Z",
                  "status_stale": false,
                  "allowed_ips": [
                    {
                      "id": "01a10e2b-5d77-7c31-b0e2-4f9a8d6c1e27",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "feed": "a",
                      "cidr": "93.184.216.34/32",
                      "label": "Studio encoder 1",
                      "expires_at": null,
                      "status": "approved",
                      "state": "active",
                      "requested_by": "editor@tv10poc.example",
                      "requested_at": "2026-10-06T07:59:00Z",
                      "decided_by": "user:noc@interhost.example",
                      "decided_at": "2026-10-06T08:00:20Z",
                      "decision_note": null
                    }
                  ],
                  "source_exceptions": [],
                  "probes": [
                    {
                      "id": "01a10e2c-0a14-7e8d-93f1-7b5c2d4e6a08",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "rev": 7,
                      "feed": "b",
                      "status": "ok",
                      "result": {
                        "tls": "verified",
                        "video": "h264 1920x1080 25/1",
                        "audio": "aac 48000 Hz 2 ch"
                      },
                      "created_at": "2026-10-06T07:58:20Z",
                      "finished_at": "2026-10-06T07:58:22Z"
                    }
                  ],
                  "legacy": {
                    "srt": "srt://ingest-poc.vustream.net:9999",
                    "rtmp": "rtmp://ingest-poc.vustream.net:1935"
                  },
                  "trusted_auto_approve": false,
                  "legacy_relay": null,
                  "migration": null,
                  "limits": {
                    "max_managed_per_encoder": 2,
                    "port_min": 10100,
                    "port_max": 10299,
                    "failover_after_s": [
                      2,
                      60
                    ],
                    "return_after_s": [
                      5,
                      3600
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "live"
        ],
        "summary": "Save ingest settings as a new draft revision (nothing reaches the encoder until apply)",
        "description": "**Required scope:** `channels:write`\n\nStores the feeds (A required, B optional backup) and the failover policy as a new revision; nothing reaches the\nencoder until `POST …/ingest/validate` and `POST …/ingest/apply`. Secrets (pull URLs, passphrases, stream keys)\nare sealed at rest and never returned; omit one to keep the stored value (same feed mode), and push_srt\npassphrases / push_rtmp stream keys are generated when none is stored (`regenerate: true` makes a new one).\nPull sources must resolve to public addresses (internal ones only with an Interhost operator exception); feed B\nmust be a different source than A. Push feeds get a port of 10100–10299 on the channel's encoder (409 when none\nis free). A missing `failover` takes the defaults. Body at most 32 KiB. Needs `channels:write`; audited as\n`channel.ingest.save`.",
        "operationId": "putChannelIngest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "feeds"
                ],
                "properties": {
                  "feeds": {
                    "type": "object",
                    "required": [
                      "a"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "a": {
                        "$ref": "#/components/schemas/IngestFeedInput"
                      },
                      "b": {
                        "$ref": "#/components/schemas/IngestFeedInput"
                      }
                    }
                  },
                  "failover": {
                    "$ref": "#/components/schemas/IngestFailover"
                  }
                }
              },
              "example": {
                "feeds": {
                  "a": {
                    "mode": "push_srt",
                    "latency_ms": 200
                  },
                  "b": {
                    "mode": "pull_hls",
                    "url": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?token=REDACTED"
                  }
                },
                "failover": {
                  "auto": true,
                  "after_s": 5,
                  "return": "auto",
                  "return_after_s": 30,
                  "slate": true
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the ingest view with the new draft as `latest`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelIngest"
                },
                "example": {
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "encoder": "enc-1",
                  "configured": true,
                  "managed": true,
                  "desired_rev": 7,
                  "applied_rev": 7,
                  "apply_state": "applied",
                  "apply_error": null,
                  "apply_at": "2026-10-06T08:01:40Z",
                  "control": "auto",
                  "latest": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "applied": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "endpoints": {
                    "a": {
                      "mode": "push_srt",
                      "url": "srt://ingest-poc.vustream.net:10104?latency=200000&streamid=tv10poc%2Fmain%2Fa",
                      "host": "ingest-poc.vustream.net",
                      "port": 10104,
                      "proto": "udp",
                      "stream_id": "tv10poc/main/a"
                    }
                  },
                  "status": {
                    "active": "a",
                    "running_rev": 7,
                    "feeds": {
                      "a": {
                        "state": "up",
                        "since": "2026-10-06T08:01:52Z",
                        "bytes_per_s": 810359,
                        "restarts": 0,
                        "listen_port": 10104
                      },
                      "b": {
                        "state": "up",
                        "since": "2026-10-06T08:01:49Z",
                        "bytes_per_s": 612004,
                        "restarts": 0
                      }
                    },
                    "extra": {
                      "control": "auto",
                      "applying": false,
                      "firewall": "ok",
                      "playlist_fresh": true
                    }
                  },
                  "status_at": "2026-10-06T08:32:10Z",
                  "status_stale": false,
                  "allowed_ips": [
                    {
                      "id": "01a10e2b-5d77-7c31-b0e2-4f9a8d6c1e27",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "feed": "a",
                      "cidr": "93.184.216.34/32",
                      "label": "Studio encoder 1",
                      "expires_at": null,
                      "status": "approved",
                      "state": "active",
                      "requested_by": "editor@tv10poc.example",
                      "requested_at": "2026-10-06T07:59:00Z",
                      "decided_by": "user:noc@interhost.example",
                      "decided_at": "2026-10-06T08:00:20Z",
                      "decision_note": null
                    }
                  ],
                  "source_exceptions": [],
                  "probes": [
                    {
                      "id": "01a10e2c-0a14-7e8d-93f1-7b5c2d4e6a08",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "rev": 7,
                      "feed": "b",
                      "status": "ok",
                      "result": {
                        "tls": "verified",
                        "video": "h264 1920x1080 25/1",
                        "audio": "aac 48000 Hz 2 ch"
                      },
                      "created_at": "2026-10-06T07:58:20Z",
                      "finished_at": "2026-10-06T07:58:22Z"
                    }
                  ],
                  "legacy": {
                    "srt": "srt://ingest-poc.vustream.net:9999",
                    "rtmp": "rtmp://ingest-poc.vustream.net:1935"
                  },
                  "trusted_auto_approve": false,
                  "legacy_relay": null,
                  "migration": null,
                  "limits": {
                    "max_managed_per_encoder": 2,
                    "port_min": 10100,
                    "port_max": 10299,
                    "failover_after_s": [
                      2,
                      60
                    ],
                    "return_after_s": [
                      5,
                      3600
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "No free ingest port on the channel's encoder (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid settings; `errors[]` names fields such as feeds.a (required), feeds.<f>.mode, feeds.<f>.url (scheme/port, credentials, not a public source, required), feeds.<f>.host, feeds.<f>.port, feeds.<f>.latency_ms, feeds.<f>.passphrase, feeds.<f>.stream_key, feeds.b.url (same source as A) or failover.*",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/ingest/validate": {
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Dry-run a revision — the channel's process plan (secrets redacted), a diff against the running revision…",
        "description": "**Required scope:** `channels:write`\n\nDry-run a revision — the channel's process plan (secrets redacted), a diff against the running revision, and connectivity checks of pull sources from the encoder (ffprobe, no ingest)\n\nBuilds the agent's process plan for the revision (`rev`, default the latest), diffs it against the running\nrevision, checks the encoder's capacity (managed channels per encoder) and that the tenant has an OME application\nthere, warns about push feeds without an approved encoder address, and queues an asynchronous connectivity check\nper pull feed (only when there are no errors). `ok` is null while checks run — follow them in `probes` of\n`GET …/ingest`; apply re-evaluates them. The result is stored on the revision and the apply state becomes\n`validated` (unless an apply is in progress). Body optional, at most 4 KiB. Needs `channels:write`; audited as\n`channel.ingest.validate`.",
        "operationId": "validateChannelIngest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rev": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "revision to validate; default the latest"
                  }
                }
              },
              "example": {
                "rev": 7
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The validation (stored on the revision)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestValidation"
                },
                "example": {
                  "ok": null,
                  "at": "2026-10-06T07:58:20Z",
                  "rev": 7,
                  "plan": [
                    "[input-a] /usr/bin/ffmpeg -hide_banner -nostdin -loglevel warning -i srt://0.0.0.0:10104?mode=listener&passphrase=***&streamid=*** … -f mpegts pipe:1",
                    "[publisher] /usr/bin/ffmpeg -hide_banner -nostdin -loglevel warning -f mpegts -i pipe:0 -c copy -f mpegts srt://127.0.0.1:9999?streamid=***&latency=200000"
                  ],
                  "diff": [
                    "+ feed b: {\"mode\":\"pull_hls\",\"url_display\":\"https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…\",\"host\":\"backup.example-broadcaster.tv\"}"
                  ],
                  "probes": [
                    "01a10e2c-0a14-7e8d-93f1-7b5c2d4e6a08"
                  ],
                  "warnings": [],
                  "errors": [],
                  "diff_against_rev": 6
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel, or no such revision",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "No ingest settings saved yet (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/ingest/apply": {
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Apply a validated revision to this channel only…",
        "description": "**Required scope:** `channels:write`\n\nApply a validated revision to this channel only (the encoder agent starts it; auto-rollback when the live playlist does not advance within 30 s)\n\nMarks the revision as desired; the encoder agent picks it up, starts this channel's processes and reports back\n(`apply_state` applying → applied, or rolled_back / failed / conflict with `apply_error`). Follow progress with\n`GET …/ingest` or the event stream. The revision must be validated with no errors and finished connectivity\nchecks. Refused with 409 while another revision is applying, while an operator relay still publishes the channel\n(use `POST …/ingest/migrate`), or when the encoder already runs its limit of managed channels. Records an\n`apply_requested` ingest event. Body at most 4 KiB. Needs `channels:write`; audited as `channel.ingest.apply`.",
        "operationId": "applyChannelIngest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rev"
                ],
                "properties": {
                  "rev": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              },
              "example": {
                "rev": 7
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Applying; the ingest view (`apply_state: applying`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelIngest"
                },
                "example": {
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "encoder": "enc-1",
                  "configured": true,
                  "managed": true,
                  "desired_rev": 7,
                  "applied_rev": 7,
                  "apply_state": "applied",
                  "apply_error": null,
                  "apply_at": "2026-10-06T08:01:40Z",
                  "control": "auto",
                  "latest": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "applied": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "endpoints": {
                    "a": {
                      "mode": "push_srt",
                      "url": "srt://ingest-poc.vustream.net:10104?latency=200000&streamid=tv10poc%2Fmain%2Fa",
                      "host": "ingest-poc.vustream.net",
                      "port": 10104,
                      "proto": "udp",
                      "stream_id": "tv10poc/main/a"
                    }
                  },
                  "status": {
                    "active": "a",
                    "running_rev": 7,
                    "feeds": {
                      "a": {
                        "state": "up",
                        "since": "2026-10-06T08:01:52Z",
                        "bytes_per_s": 810359,
                        "restarts": 0,
                        "listen_port": 10104
                      },
                      "b": {
                        "state": "up",
                        "since": "2026-10-06T08:01:49Z",
                        "bytes_per_s": 612004,
                        "restarts": 0
                      }
                    },
                    "extra": {
                      "control": "auto",
                      "applying": false,
                      "firewall": "ok",
                      "playlist_fresh": true
                    }
                  },
                  "status_at": "2026-10-06T08:32:10Z",
                  "status_stale": false,
                  "allowed_ips": [
                    {
                      "id": "01a10e2b-5d77-7c31-b0e2-4f9a8d6c1e27",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "feed": "a",
                      "cidr": "93.184.216.34/32",
                      "label": "Studio encoder 1",
                      "expires_at": null,
                      "status": "approved",
                      "state": "active",
                      "requested_by": "editor@tv10poc.example",
                      "requested_at": "2026-10-06T07:59:00Z",
                      "decided_by": "user:noc@interhost.example",
                      "decided_at": "2026-10-06T08:00:20Z",
                      "decision_note": null
                    }
                  ],
                  "source_exceptions": [],
                  "probes": [
                    {
                      "id": "01a10e2c-0a14-7e8d-93f1-7b5c2d4e6a08",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "rev": 7,
                      "feed": "b",
                      "status": "ok",
                      "result": {
                        "tls": "verified",
                        "video": "h264 1920x1080 25/1",
                        "audio": "aac 48000 Hz 2 ch"
                      },
                      "created_at": "2026-10-06T07:58:20Z",
                      "finished_at": "2026-10-06T07:58:22Z"
                    }
                  ],
                  "legacy": {
                    "srt": "srt://ingest-poc.vustream.net:9999",
                    "rtmp": "rtmp://ingest-poc.vustream.net:1935"
                  },
                  "trusted_auto_approve": false,
                  "legacy_relay": null,
                  "migration": null,
                  "limits": {
                    "max_managed_per_encoder": 2,
                    "port_min": 10100,
                    "port_max": 10299,
                    "failover_after_s": [
                      2,
                      60
                    ],
                    "return_after_s": [
                      5,
                      3600
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel, or no such revision",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "No settings saved; another revision is applying; the revision is not validated (errors, checks running or failed, replaced checks); an operator relay still runs the channel; or the encoder's managed-channel limit is reached",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`rev` missing, not positive, or the body is not JSON",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/ingest/disable": {
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Stop managing the channel's ingest from Studio (the agent stops its processes)",
        "description": "**Required scope:** `channels:write`\n\nSets `apply_state: disabled` and `managed: false`; the encoder agent stops this channel's ingest processes on its\nnext poll (viewers lose the live feed unless another source publishes it). Saved revisions stay and can be\napplied again. Records a `disabled` ingest event. Needs `channels:write`; audited as `channel.ingest.disable`.",
        "operationId": "disableChannelIngest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Disabled; the ingest view",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelIngest"
                },
                "example": {
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "encoder": "enc-1",
                  "configured": true,
                  "managed": true,
                  "desired_rev": 7,
                  "applied_rev": 7,
                  "apply_state": "applied",
                  "apply_error": null,
                  "apply_at": "2026-10-06T08:01:40Z",
                  "control": "auto",
                  "latest": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "applied": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "endpoints": {
                    "a": {
                      "mode": "push_srt",
                      "url": "srt://ingest-poc.vustream.net:10104?latency=200000&streamid=tv10poc%2Fmain%2Fa",
                      "host": "ingest-poc.vustream.net",
                      "port": 10104,
                      "proto": "udp",
                      "stream_id": "tv10poc/main/a"
                    }
                  },
                  "status": {
                    "active": "a",
                    "running_rev": 7,
                    "feeds": {
                      "a": {
                        "state": "up",
                        "since": "2026-10-06T08:01:52Z",
                        "bytes_per_s": 810359,
                        "restarts": 0,
                        "listen_port": 10104
                      },
                      "b": {
                        "state": "up",
                        "since": "2026-10-06T08:01:49Z",
                        "bytes_per_s": 612004,
                        "restarts": 0
                      }
                    },
                    "extra": {
                      "control": "auto",
                      "applying": false,
                      "firewall": "ok",
                      "playlist_fresh": true
                    }
                  },
                  "status_at": "2026-10-06T08:32:10Z",
                  "status_stale": false,
                  "allowed_ips": [
                    {
                      "id": "01a10e2b-5d77-7c31-b0e2-4f9a8d6c1e27",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "feed": "a",
                      "cidr": "93.184.216.34/32",
                      "label": "Studio encoder 1",
                      "expires_at": null,
                      "status": "approved",
                      "state": "active",
                      "requested_by": "editor@tv10poc.example",
                      "requested_at": "2026-10-06T07:59:00Z",
                      "decided_by": "user:noc@interhost.example",
                      "decided_at": "2026-10-06T08:00:20Z",
                      "decision_note": null
                    }
                  ],
                  "source_exceptions": [],
                  "probes": [
                    {
                      "id": "01a10e2c-0a14-7e8d-93f1-7b5c2d4e6a08",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "rev": 7,
                      "feed": "b",
                      "status": "ok",
                      "result": {
                        "tls": "verified",
                        "video": "h264 1920x1080 25/1",
                        "audio": "aac 48000 Hz 2 ch"
                      },
                      "created_at": "2026-10-06T07:58:20Z",
                      "finished_at": "2026-10-06T07:58:22Z"
                    }
                  ],
                  "legacy": {
                    "srt": "srt://ingest-poc.vustream.net:9999",
                    "rtmp": "rtmp://ingest-poc.vustream.net:1935"
                  },
                  "trusted_auto_approve": false,
                  "legacy_relay": null,
                  "migration": null,
                  "limits": {
                    "max_managed_per_encoder": 2,
                    "port_min": 10100,
                    "port_max": 10299,
                    "failover_after_s": [
                      2,
                      60
                    ],
                    "return_after_s": [
                      5,
                      3600
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel, or it has no ingest settings",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/ingest/switch": {
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Switch the active feed (a | b) or return to automatic failover — audited",
        "description": "**Required scope:** `channels:operate`\n\nStores an operator command the encoder agent executes on its next poll (asynchronous: 202). `a` / `b` pins the\nfeed (409 when it is not configured in the running revision, or not `up` unless `force` — then the slate plays\nuntil it is); `auto` hands control back to the failover policy. Only for channels whose ingest runs from applied\nStudio settings. Records a `switch_requested` / `auto_requested` ingest event. Body at most 4 KiB.\nNeeds `channels:operate`; audited as `channel.ingest.control`.",
        "operationId": "switchChannelIngest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "enum": [
                      "a",
                      "b",
                      "auto"
                    ]
                  },
                  "force": {
                    "type": "boolean",
                    "default": false,
                    "description": "switch even when the feed is down (the slate plays until it is up)"
                  }
                }
              },
              "example": {
                "to": "b"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Command stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestControlResult"
                },
                "example": {
                  "control": "b",
                  "control_seq": 42
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The channel's ingest is not running from Studio settings (never applied, disabled or rolled back); the feed is not configured; or it is not up and `force` is false",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`to` is not a, b or auto, or the body is not JSON",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:operate"
      }
    },
    "/v1/channels/{id}/ingest/slate": {
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Play the slate (on) or resume the live feed (off) — audited",
        "description": "**Required scope:** `channels:operate`\n\n`on: true` stores the `slate` command; `on: false` hands control back to automatic failover (`auto`). The\nencoder agent executes it on its next poll (202). Only for channels whose ingest runs from applied Studio\nsettings. Records a `slate_requested` / `resume_requested` ingest event. Body at most 4 KiB.\nNeeds `channels:operate`; audited as `channel.ingest.control`.",
        "operationId": "slateChannelIngest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "on"
                ],
                "properties": {
                  "on": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "on": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Command stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestControlResult"
                },
                "example": {
                  "control": "slate",
                  "control_seq": 43
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The channel's ingest is not running from Studio settings (never applied, disabled or rolled back)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`on` missing or the body is not JSON",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:operate"
      }
    },
    "/v1/channels/{id}/ingest/restart": {
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Restart this channel's ingest publisher only (never OME, never another channel) — audited",
        "description": "**Required scope:** `channels:operate`\n\nQueues a restart of the one ffmpeg that publishes this channel into OME; the encoder agent executes it on its next\npoll (202) and viewers see a short gap. Only for channels whose ingest runs from applied Studio settings. Records\na `restart_requested` ingest event. Needs `channels:operate`; audited as `channel.ingest.restart_publisher`.",
        "operationId": "restartChannelIngestPublisher",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Restart queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "restart_seq"
                  ],
                  "properties": {
                    "restart_seq": {
                      "type": "integer",
                      "description": "id of the newest restart request (the agent restarts when it sees a higher one)"
                    }
                  }
                },
                "example": {
                  "restart_seq": 5120
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The channel's ingest is not running from Studio settings (never applied, disabled or rolled back)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:operate"
      }
    },
    "/v1/channels/{id}/ingest/migrate": {
      "post": {
        "tags": [
          "live"
        ],
        "summary": "Move the channel from the operator relay to Studio ingest",
        "description": "**Required scope:** `channels:write`\n\nMove the channel from the operator relay to Studio ingest — a draft with feed A pre-filled from the relay source (sealed) and a migration request an Interhost operator approves; nothing changes on air before the cut-over\n\nFor a channel that still runs from an operator relay: creates a draft revision with feed A = the relay's HLS\nsource (pull_hls; the sealed URL is copied, never shown) and the default failover, plus a migration request\n(`status: requested`). An Interhost operator validates, ticks the checklist and approves; the encoder agent then\ncuts over. Nothing changes on air before that. Records a `migration_requested` ingest event. Needs\n`channels:write`; audited as `channel.ingest.migration_requested`.",
        "operationId": "migrateChannelIngest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Requested; the ingest view with the new draft and `migration`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelIngest"
                },
                "example": {
                  "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                  "encoder": "enc-1",
                  "configured": true,
                  "managed": false,
                  "desired_rev": 7,
                  "applied_rev": 7,
                  "apply_state": "draft",
                  "apply_error": null,
                  "apply_at": "2026-10-06T08:01:40Z",
                  "control": "auto",
                  "latest": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "applied": {
                    "rev": 7,
                    "config": {
                      "feeds": {
                        "a": {
                          "mode": "push_srt",
                          "stream_id": "tv10poc/main/a",
                          "latency_ms": 200
                        },
                        "b": {
                          "mode": "pull_hls",
                          "url_display": "https://backup.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                          "host": "backup.example-broadcaster.tv"
                        }
                      },
                      "failover": {
                        "auto": true,
                        "after_s": 5,
                        "return": "auto",
                        "return_after_s": 30,
                        "slate": true,
                        "triggers": {},
                        "min_hold_s": 60,
                        "max_switches_per_hour": 6
                      }
                    },
                    "secrets_set": {
                      "a.passphrase": true,
                      "b.url": true
                    },
                    "created_by": "editor@tv10poc.example",
                    "created_at": "2026-10-06T07:58:12Z"
                  },
                  "endpoints": {
                    "a": {
                      "mode": "push_srt",
                      "url": "srt://ingest-poc.vustream.net:10104?latency=200000&streamid=tv10poc%2Fmain%2Fa",
                      "host": "ingest-poc.vustream.net",
                      "port": 10104,
                      "proto": "udp",
                      "stream_id": "tv10poc/main/a"
                    }
                  },
                  "status": {
                    "active": "a",
                    "running_rev": 7,
                    "feeds": {
                      "a": {
                        "state": "up",
                        "since": "2026-10-06T08:01:52Z",
                        "bytes_per_s": 810359,
                        "restarts": 0,
                        "listen_port": 10104
                      },
                      "b": {
                        "state": "up",
                        "since": "2026-10-06T08:01:49Z",
                        "bytes_per_s": 612004,
                        "restarts": 0
                      }
                    },
                    "extra": {
                      "control": "auto",
                      "applying": false,
                      "firewall": "ok",
                      "playlist_fresh": true
                    }
                  },
                  "status_at": "2026-10-06T08:32:10Z",
                  "status_stale": false,
                  "allowed_ips": [
                    {
                      "id": "01a10e2b-5d77-7c31-b0e2-4f9a8d6c1e27",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "feed": "a",
                      "cidr": "93.184.216.34/32",
                      "label": "Studio encoder 1",
                      "expires_at": null,
                      "status": "approved",
                      "state": "active",
                      "requested_by": "editor@tv10poc.example",
                      "requested_at": "2026-10-06T07:59:00Z",
                      "decided_by": "user:noc@interhost.example",
                      "decided_at": "2026-10-06T08:00:20Z",
                      "decision_note": null
                    }
                  ],
                  "source_exceptions": [],
                  "probes": [
                    {
                      "id": "01a10e2c-0a14-7e8d-93f1-7b5c2d4e6a08",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "rev": 7,
                      "feed": "b",
                      "status": "ok",
                      "result": {
                        "tls": "verified",
                        "video": "h264 1920x1080 25/1",
                        "audio": "aac 48000 Hz 2 ch"
                      },
                      "created_at": "2026-10-06T07:58:20Z",
                      "finished_at": "2026-10-06T07:58:22Z"
                    }
                  ],
                  "legacy": {
                    "srt": "srt://ingest-poc.vustream.net:9999",
                    "rtmp": "rtmp://ingest-poc.vustream.net:1935"
                  },
                  "trusted_auto_approve": false,
                  "legacy_relay": {
                    "instance": "tv10poc_main",
                    "url_display": "https://origin.example-broadcaster.tv/live/ch10/playlist.m3u8?…",
                    "unit_active": true,
                    "unit_enabled": true,
                    "seen_at": "2026-10-06T08:30:00Z"
                  },
                  "migration": {
                    "id": "01a10e51-2f3a-7b6c-8d9e-0a1b2c3d4e5f",
                    "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                    "rev": 8,
                    "instance": "tv10poc_main",
                    "status": "requested",
                    "checklist": {},
                    "requested_by": "editor@tv10poc.example",
                    "requested_at": "2026-10-06T08:33:00Z",
                    "approved_by": null,
                    "approved_at": null,
                    "finished_at": null,
                    "error": null
                  },
                  "limits": {
                    "max_managed_per_encoder": 2,
                    "port_min": 10100,
                    "port_max": 10299,
                    "failover_after_s": [
                      2,
                      60
                    ],
                    "return_after_s": [
                      5,
                      3600
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "No operator relay is known for the channel yet; it already runs from Studio ingest; the relay source is not an HLS URL (configure feed A by hand); or a migration is already open",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings (migration 0048) or ingest migrations (migration 0052) are not available on this node (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/ingest/events": {
      "get": {
        "tags": [
          "live"
        ],
        "summary": "Failover history (failover, return, manual switch, slate, apply, rollback)",
        "description": "**Required scope:** `channels:read`\n\nThe channel's ingest events, newest first: the agent's automatic decisions (failover, return, slate on/off,\nblocked failovers), operator commands, applies and their outcome, encoder address requests and migrations.\nNot paged; `limit` caps the count (an invalid value falls back to 100). Needs `channels:read`.",
        "operationId": "listChannelIngestEvents",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum events (1–500; other values mean 100)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Events, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/IngestEvent"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": 5121,
                      "at": "2026-10-06T08:40:12Z",
                      "kind": "failover",
                      "from": "a",
                      "to": "b",
                      "actor": "agent:enc-1",
                      "detail": {
                        "auto": true,
                        "code": "lost",
                        "reason": "feed A lost for 5 s"
                      }
                    },
                    {
                      "id": 5098,
                      "at": "2026-10-06T08:01:40Z",
                      "kind": "apply_requested",
                      "from": null,
                      "to": null,
                      "actor": "editor@tv10poc.example",
                      "detail": {
                        "rev": 7,
                        "from_rev": 6
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant (unknown, malformed or trashed id)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/ingest/feeds/{feed}/allowed-ips": {
      "put": {
        "tags": [
          "live"
        ],
        "summary": "Encoder source addresses of a push feed",
        "description": "**Required scope:** `channels:write`\n\nEncoder source addresses of a push feed — new ones apply after an Interhost operator approves them (at once for a trusted tenant); removed ones close at once; only the feed's own port opens\n\nReplaces the feed's address list (at most 10). New addresses are `pending` until an Interhost operator approves\nthem (approved at once for a tenant an operator marked trusted); addresses left out are revoked immediately;\nlabel / expiry changes update in place. Each entry is an IPv4 address or prefix /24 or narrower, or IPv6 /56 or\nnarrower, public unicast only (private, loopback, link-local, multicast and reserved ranges — including the\ndocumentation ranges — are refused). Only the feed's own port opens, never the shared OME listener. Records\n`ip_requested` / `ip_approved` / `ip_revoked` ingest events. Body at most 16 KiB. Needs `channels:write`;\naudited as `channel.ingest.allowed_ips`.",
        "operationId": "putChannelIngestAllowedIPs",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "feed",
            "in": "path",
            "required": true,
            "description": "Push feed (`a` or `b`) of the latest revision",
            "schema": {
              "type": "string",
              "enum": [
                "a",
                "b"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ips"
                ],
                "properties": {
                  "ips": {
                    "type": "array",
                    "maxItems": 10,
                    "description": "the complete list for the feed (an empty list revokes every address)",
                    "items": {
                      "type": "object",
                      "required": [
                        "cidr"
                      ],
                      "properties": {
                        "cidr": {
                          "type": "string",
                          "example": "93.184.216.34",
                          "description": "IPv4 (/24 or narrower) or IPv6 (/56 or narrower), public only; listed once"
                        },
                        "label": {
                          "type": "string",
                          "maxLength": 100
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "optional, in the future and at most a year ahead; the address closes by itself"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "ips": [
                  {
                    "cidr": "93.184.216.34",
                    "label": "Studio encoder 1"
                  },
                  {
                    "cidr": "93.184.216.0/28",
                    "label": "OB van",
                    "expires_at": "2026-10-13T00:00:00Z"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The channel's address list after the change, what changed, and whether the tenant is trusted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "change",
                    "trusted"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/IngestAllowedIP"
                      },
                      "description": "all feeds of the channel: pending and approved, plus ones decided in the last 7 days"
                    },
                    "change": {
                      "type": "object",
                      "properties": {
                        "added": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "new addresses (pending, or approved at once when trusted)"
                        },
                        "approved": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "auto-approved (trusted tenant)"
                        },
                        "removed": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "revoked at once"
                        },
                        "updated": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "label or expiry changed"
                        }
                      }
                    },
                    "trusted": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01a10e2b-5d77-7c31-b0e2-4f9a8d6c1e27",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "feed": "a",
                      "cidr": "93.184.216.34/32",
                      "label": "Studio encoder 1",
                      "expires_at": null,
                      "status": "approved",
                      "state": "active",
                      "requested_by": "editor@tv10poc.example",
                      "requested_at": "2026-10-06T07:59:00Z",
                      "decided_by": "user:noc@interhost.example",
                      "decided_at": "2026-10-06T08:00:20Z",
                      "decision_note": null
                    },
                    {
                      "id": "01a10e5a-6b7c-7d8e-9f0a-1b2c3d4e5f60",
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "feed": "a",
                      "cidr": "93.184.216.0/28",
                      "label": "OB van",
                      "expires_at": "2026-10-13T00:00:00Z",
                      "status": "pending",
                      "state": "pending",
                      "requested_by": "editor@tv10poc.example",
                      "requested_at": "2026-10-06T08:41:00Z",
                      "decided_by": null,
                      "decided_at": null,
                      "decision_note": null
                    }
                  ],
                  "change": {
                    "added": [
                      "93.184.216.0/28"
                    ],
                    "approved": [],
                    "removed": [],
                    "updated": []
                  },
                  "trusted": false
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Scope missing (`insufficient_scope`) or a CDN-only tenant (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel, or `feed` is not a or b",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The feed is not a push feed in the latest revision (or no settings are saved)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "More than 10 addresses, or an entry is invalid (`ips[i].cidr` malformed, too wide, not public or listed twice; `ips[i].label` too long; `ips[i].expires_at` not in the future or more than a year ahead)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Ingest settings are not enabled on this node (`feature_disabled`, migration 0048 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/jobs": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "The tenant's own jobs (newest first), cursor pagination — Studio Dashboard \"Pipeline\"",
        "description": "**Required scope:** `assets:read`\n\nLists the tenant's processing jobs (probe, transcode, thumbnails, subtitles, posters, …) newest first by\n`created_at`. Filter by `status` and `type`; page with `limit` and the `next_cursor` of the previous page\n(null on the last page; a malformed cursor is ignored and returns the first page). Internal service addresses\nin `error` are replaced with `[internal]`. Jobs without a tenant (platform maintenance) are never listed.\nPlatform tenants only. For one job with its event log use `GET /v1/jobs/{id}`; for failures grouped by reason use\n`GET /v1/jobs/failures`.",
        "operationId": "listJobs",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Only jobs in this status",
            "schema": {
              "type": "string",
              "enum": [
                "queued",
                "dispatched",
                "running",
                "succeeded",
                "failed",
                "cancelled"
              ]
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Only jobs of this type",
            "schema": {
              "$ref": "#/components/schemas/JobType"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "`next_cursor` of the previous page (`<created_at RFC 3339>|<job id>`)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of jobs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "next_cursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Job"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01a10c2e-5b7f-7d21-9e44-8c1a2b3c4d5e",
                      "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "type": "thumbs",
                      "priority": 2,
                      "status": "succeeded",
                      "payload": {
                        "poster": true,
                        "sprite": {
                          "h": 90,
                          "w": 160,
                          "tile": "10x10",
                          "interval_s": 5
                        },
                        "asset_id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                        "thumbs_vtt": true,
                        "duration_ms": 1785600
                      },
                      "result": {
                        "tiles": 358,
                        "sheets": 4
                      },
                      "error": null,
                      "attempts": 1,
                      "max_attempts": 3,
                      "worker_id": "xcode-1",
                      "lease_until": null,
                      "asset_id": "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                      "clip_id": null,
                      "channel_id": null,
                      "created_at": "2026-10-06T07:12:03Z",
                      "dispatched_at": "2026-10-06T07:12:04Z",
                      "started_at": "2026-10-06T07:12:05Z",
                      "finished_at": "2026-10-06T07:12:41Z"
                    }
                  ],
                  "next_cursor": "2026-10-06T07:12:03.118204Z|01a10c2e-5b7f-7d21-9e44-8c1a2b3c4d5e"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `invalid query`: unknown status or type, or limit outside 1–200",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "One of the tenant's own jobs with its events",
        "description": "**Required scope:** `assets:read` or `delivery:write` or `prewarm`\n\nReturns the job and its event log (progress messages, retries), oldest event first. Internal service\naddresses in `error` and in event messages are replaced with `[internal]`. Use it to follow up the\n`job_id` an asynchronous (202) call returned, or watch `job.status` on the event stream / webhooks instead\nof polling. Callable with any of `assets:read`, `delivery:write` or `prewarm` (pre-warm and purge jobs).\nAnother tenant's job, or a job without a tenant, is 404. Platform tenants only.",
        "operationId": "getJob",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Job id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobWithEvents"
                },
                "example": {
                  "id": "01a10c2e-5b7f-7d21-9e44-8c1a2b3c4d5e",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "type": "subtitles",
                  "priority": 3,
                  "status": "running",
                  "payload": {
                    "title": "מבזק",
                    "bucket": "rec",
                    "language": "he"
                  },
                  "result": null,
                  "error": null,
                  "attempts": 1,
                  "max_attempts": 2,
                  "worker_id": "xcode-1",
                  "lease_until": "2026-10-06T07:20:15Z",
                  "asset_id": null,
                  "clip_id": null,
                  "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                  "created_at": "2026-10-06T07:14:58Z",
                  "dispatched_at": "2026-10-06T07:15:00Z",
                  "started_at": "2026-10-06T07:15:02Z",
                  "finished_at": null,
                  "events": [
                    {
                      "id": 918231,
                      "job_id": "01a10c2e-5b7f-7d21-9e44-8c1a2b3c4d5e",
                      "at": "2026-10-06T07:14:58Z",
                      "level": "info",
                      "message": "created"
                    },
                    {
                      "id": 918240,
                      "job_id": "01a10c2e-5b7f-7d21-9e44-8c1a2b3c4d5e",
                      "at": "2026-10-06T07:16:12Z",
                      "level": "info",
                      "message": "progress: transcribing",
                      "data": {
                        "percent": 41,
                        "worker_id": "xcode-1"
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — no such job in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read or delivery:write or prewarm"
      }
    },
    "/v1/jobs/failures": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "The tenant's failed jobs grouped by type and reason (Studio Dashboard failed-jobs drawer)",
        "operationId": "listJobFailures",
        "description": "**Required scope:** `assets:read`\n\nFailed jobs of the last `hours` (default 24; at most 1000 jobs read), grouped by job type + reason code, each\nwith a Hebrew and English explanation, the item it was about, and its state: `open`, `superseded` (a later job\nfor the same item succeeded), `retrying` (a later job is queued or running), `resolved` (an environment cause\nfixed since: a later job of the same type succeeded), `waiting` (a shared service such as ASR or translation is\ndown; re-queued automatically when it answers) or `permanent` (nothing to retry, e.g. no recording for the\nwindow, or the item was deleted). `open` counts only items that still need attention; groups with open items\ncome first, then by latest failure. Errors are shown with internal addresses replaced by `[internal]`.\nFailures marked ignored (POST /v1/jobs/{id}/ignore) are left out and counted in `ignored`; `ignored=1` lists\nthem too, with state `ignored`. `retryable` / `not_retryable_reason` say whether POST /v1/jobs/{id}/retry\nwill accept the job. Platform tenants only.",
        "parameters": [
          {
            "name": "hours",
            "in": "query",
            "description": "Look-back window in hours",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 168,
              "default": 24
            }
          },
          {
            "name": "ignored",
            "in": "query",
            "description": "1 = also list failures marked ignored",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Groups of failed jobs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "since",
                    "hours",
                    "total",
                    "ignored",
                    "open",
                    "superseded",
                    "retrying",
                    "waiting",
                    "resolved",
                    "permanent",
                    "groups"
                  ],
                  "properties": {
                    "since": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "hours": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "ignored": {
                      "type": "integer",
                      "description": "failures marked ignored in the window (left out unless ignored=1)"
                    },
                    "open": {
                      "type": "integer"
                    },
                    "superseded": {
                      "type": "integer"
                    },
                    "retrying": {
                      "type": "integer"
                    },
                    "resolved": {
                      "type": "integer"
                    },
                    "waiting": {
                      "type": "integer",
                      "description": "failures parked until a shared service answers"
                    },
                    "permanent": {
                      "type": "integer"
                    },
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "code": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "environment",
                              "subject",
                              "waiting",
                              "permanent"
                            ]
                          },
                          "retryable": {
                            "type": "boolean"
                          },
                          "title_he": {
                            "type": "string"
                          },
                          "title_en": {
                            "type": "string"
                          },
                          "help_he": {
                            "type": "string"
                          },
                          "help_en": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          },
                          "open": {
                            "type": "integer"
                          },
                          "resolved": {
                            "type": "boolean"
                          },
                          "resolved_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "first_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "last_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "retryable_count": {
                            "type": "integer"
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "type": {
                                  "type": "string"
                                },
                                "created_at": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "finished_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "format": "date-time"
                                },
                                "attempts": {
                                  "type": "integer"
                                },
                                "max_attempts": {
                                  "type": "integer"
                                },
                                "error": {
                                  "type": "string"
                                },
                                "state": {
                                  "type": "string",
                                  "enum": [
                                    "open",
                                    "superseded",
                                    "retrying",
                                    "resolved",
                                    "permanent",
                                    "waiting",
                                    "ignored"
                                  ]
                                },
                                "retryable": {
                                  "type": "boolean"
                                },
                                "not_retryable_reason": {
                                  "type": "string",
                                  "enum": [
                                    "type",
                                    "permanent",
                                    "gone",
                                    "superseded",
                                    "automatic"
                                  ],
                                  "description": "absent when retryable; type = re-run from the item, not per job; automatic = re-queued by the platform"
                                },
                                "later_job_id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "later_job_status": {
                                  "type": "string"
                                },
                                "ignored_at": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "ignored_by": {
                                  "type": "string"
                                },
                                "ignore_note": {
                                  "type": "string"
                                },
                                "channel_id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "subject": {
                                  "type": "object",
                                  "properties": {
                                    "kind": {
                                      "type": "string"
                                    },
                                    "id": {
                                      "type": [
                                        "string",
                                        "null"
                                      ],
                                      "format": "uuid"
                                    },
                                    "title": {
                                      "type": [
                                        "string",
                                        "null"
                                      ]
                                    },
                                    "gone": {
                                      "type": "boolean"
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "since": "2026-10-05T08:00:00Z",
                  "hours": 24,
                  "total": 3,
                  "ignored": 0,
                  "open": 1,
                  "superseded": 1,
                  "retrying": 0,
                  "waiting": 0,
                  "resolved": 0,
                  "permanent": 1,
                  "groups": [
                    {
                      "key": "content_summary/other",
                      "code": "other",
                      "kind": "subject",
                      "retryable": true,
                      "title_he": "שגיאה אחרת",
                      "title_en": "Other error",
                      "help_he": "שגיאה שאין לה עדיין הסבר מוכן. הפרטים הטכניים מראים את ההודעה המקורית.",
                      "help_en": "An error without a prepared explanation yet. The technical details show the original message.",
                      "type": "content_summary",
                      "count": 2,
                      "open": 1,
                      "resolved": false,
                      "first_at": "2026-10-05T19:40:12Z",
                      "last_at": "2026-10-06T06:02:51Z",
                      "retryable_count": 1,
                      "items": [
                        {
                          "id": "01a10a77-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
                          "type": "content_summary",
                          "created_at": "2026-10-06T05:58:10Z",
                          "finished_at": "2026-10-06T06:02:51Z",
                          "attempts": 3,
                          "max_attempts": 3,
                          "error": "the notes model returned no usable notes (0 words) — retry later",
                          "subject": {
                            "kind": "programme",
                            "id": "01a10a70-9e8f-7a6b-8c5d-4e3f2a1b0c9d",
                            "title": "מבזק",
                            "gone": false
                          },
                          "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                          "state": "open",
                          "retryable": true
                        },
                        {
                          "id": "01a0fd31-6b5a-7c4d-9e3f-2a1b0c9d8e7f",
                          "type": "content_summary",
                          "created_at": "2026-10-05T19:36:40Z",
                          "finished_at": "2026-10-05T19:40:12Z",
                          "attempts": 3,
                          "max_attempts": 3,
                          "error": "the notes model returned no usable notes (14 words) — retry later",
                          "subject": {
                            "kind": "programme",
                            "id": "01a0fd2c-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
                            "title": "בוקר כלכלי",
                            "gone": false
                          },
                          "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                          "state": "superseded",
                          "retryable": false,
                          "not_retryable_reason": "superseded",
                          "later_job_id": "01a0fe02-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
                          "later_job_status": "succeeded"
                        }
                      ]
                    },
                    {
                      "key": "subtitles/no_recording",
                      "code": "no_recording",
                      "kind": "permanent",
                      "retryable": false,
                      "title_he": "אין הקלטה לחלון הזה",
                      "title_en": "No recording for this window",
                      "help_he": "החלון המבוקש כבר מחוץ לתקופת השמירה של ההקלטות, או שהערוץ לא הוקלט בזמן הזה. אין מה לנסות שוב.",
                      "help_en": "The requested window is past the recording retention, or the channel was not recorded then. Nothing to retry.",
                      "type": "subtitles",
                      "count": 1,
                      "open": 0,
                      "resolved": true,
                      "first_at": "2026-10-05T11:20:04Z",
                      "last_at": "2026-10-05T11:20:04Z",
                      "retryable_count": 0,
                      "items": [
                        {
                          "id": "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                          "type": "subtitles",
                          "created_at": "2026-10-05T11:19:58Z",
                          "finished_at": "2026-10-05T11:20:04Z",
                          "attempts": 1,
                          "max_attempts": 2,
                          "error": "no_recording: playlist HTTP 422: no recording in the requested range",
                          "subject": {
                            "kind": "programme",
                            "id": "01a0f9b0-5e4d-7c3b-8a29-1f0e9d8c7b6a",
                            "title": "שבע עם יהודה שלזינגר",
                            "gone": false
                          },
                          "channel_id": "0192b0c4-2a1d-7e3f-8a4b-1c2d3e4f5a6b",
                          "state": "permanent",
                          "retryable": false,
                          "not_retryable_reason": "permanent"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — hours outside 1–168",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/jobs/{id}/retry": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Retry one of the tenant's failed jobs (assets:write; idempotent, audited)",
        "operationId": "retryTenantJob",
        "description": "**Required scope:** `assets:write`\n\nRe-queues a failed or cancelled job of a retryable type (preview, poster_select, podcast_audio, subtitles,\nsubtitles_translate, programme_bounds, image_upscale, lipsync_sample, thumbs, content_summary): attempts reset\nto 0, error, result and any ignored mark are cleared. A job that is already queued, running or succeeded is\nanswered 200 with `retried: false` and a `note` (idempotent). Other types (re-run from their item), permanent\nreasons and jobs whose asset was deleted answer 422. On success emits `job.status` (SSE + webhooks) and is\naudited as `job.retry`; follow the job with `GET /v1/jobs/{id}`. Platform tenants only.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Job id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Outcome: re-queued (`retried: true`, status `queued`) or already queued/running/succeeded (`retried: false` + note)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobRetryOutcome"
                },
                "example": {
                  "id": "01a10a77-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
                  "status": "queued",
                  "retried": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — no such job in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error` — the job type is not retried one by one, the failure reason is permanent, or the asset was deleted",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "not retryable: No recording for this window",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/jobs/{id}/ignore": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Mark one of the tenant's failed jobs ignored (assets:write; audited)",
        "operationId": "ignoreTenantJob",
        "description": "**Required scope:** `assets:write`\n\nThe job keeps status `failed`; it leaves the failed-jobs list (shown with `ignored=1`), the JobsFailing alert\nand the tenant failure monitors. Marking again replaces the actor and note. A retry clears the mark. The body\nis optional (an empty body = no note). Audited as `job.ignore`. Platform tenants only.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Job id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Optional JSON object, at most 64 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 300,
                    "description": "Why it needs no attention (characters)"
                  }
                }
              },
              "example": {
                "note": "test upload, source deleted on purpose"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Marked ignored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobIgnoreOutcome"
                },
                "example": {
                  "id": "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                  "ignored": true
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 64 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — no such job in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict` — only failed jobs can be (un)ignored (the job is queued, running, succeeded or cancelled, or stopped being failed meanwhile)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error` — note longer than 300 characters",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/jobs/{id}/unignore": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Clear the ignored mark of a failed job (assets:write; audited)",
        "operationId": "unignoreTenantJob",
        "description": "**Required scope:** `assets:write`\n\nThe failure counts again in the failed-jobs list, the JobsFailing alert and the failure monitors. Clearing a\njob that was not marked still answers 200 (`ignored: false`). The job must still be `failed`. A body, if\nsent, is read like the ignore body and its note discarded. Audited as `job.unignore`. Platform tenants only.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Job id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Mark cleared",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobIgnoreOutcome"
                },
                "example": {
                  "id": "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                  "ignored": false
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — a body was sent that is not valid JSON or has unknown fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — no such job in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`conflict` — only failed jobs can be (un)ignored (the job is queued, running, succeeded or cancelled, or stopped being failed meanwhile)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/jobs/ignore": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Mark a group of the tenant's failed jobs ignored (assets:write; audited per job)",
        "operationId": "ignoreTenantJobs",
        "description": "**Required scope:** `assets:write`\n\nMarks each failed job ignored (see `POST /v1/jobs/{id}/ignore`) with the same note; duplicates are collapsed, at most 200 ids. Always 200 with one outcome per id: a job of another tenant\nor unknown gets `error: \"no such job\"`, a job that is not `failed` gets the conflict text; `changed` counts\nthe jobs updated. Each change is audited as `job.ignore`. Platform tenants only.",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 64 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 200,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 300,
                    "description": "Applied to every job of the group (characters)"
                  }
                }
              },
              "example": {
                "ids": [
                  "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                  "01a0f9c2-8e7d-7c6b-9a5f-4e3d2c1b0a9f"
                ],
                "note": "channel was off air"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Outcomes, in request order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "changed",
                    "results"
                  ],
                  "properties": {
                    "changed": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JobIgnoreOutcome"
                      }
                    }
                  }
                },
                "example": {
                  "changed": 1,
                  "results": [
                    {
                      "id": "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                      "ignored": true
                    },
                    {
                      "id": "01a0f9c2-8e7d-7c6b-9a5f-4e3d2c1b0a9f",
                      "ignored": false,
                      "error": "only failed jobs can be ignored (status succeeded)"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 64 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `ids` empty or more than 200, or note longer than 300 characters",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/jobs/unignore": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Clear the ignored mark of a group of failed jobs (assets:write; audited per job)",
        "operationId": "unignoreTenantJobs",
        "description": "**Required scope:** `assets:write`\n\nClears the ignored mark of each failed job (see `POST /v1/jobs/{id}/unignore`); duplicates are collapsed, at most 200 ids. Always 200 with one outcome per id: a job of another tenant\nor unknown gets `error: \"no such job\"`, a job that is not `failed` gets the conflict text; `changed` counts\nthe jobs updated. Each change is audited as `job.unignore`. Platform tenants only.",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 64 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 200,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 300,
                    "description": "Accepted and discarded"
                  }
                }
              },
              "example": {
                "ids": [
                  "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                  "01a0f9c2-8e7d-7c6b-9a5f-4e3d2c1b0a9f"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Outcomes, in request order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "changed",
                    "results"
                  ],
                  "properties": {
                    "changed": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JobIgnoreOutcome"
                      }
                    }
                  }
                },
                "example": {
                  "changed": 1,
                  "results": [
                    {
                      "id": "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                      "ignored": false
                    },
                    {
                      "id": "01a0f9c2-8e7d-7c6b-9a5f-4e3d2c1b0a9f",
                      "ignored": false,
                      "error": "only failed jobs can be ignored (status succeeded)"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 64 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `ids` empty or more than 200, or note longer than 300 characters",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/jobs/retry": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Retry a group of the tenant's failed jobs (assets:write; idempotent, audited per job)",
        "operationId": "retryTenantJobs",
        "description": "**Required scope:** `assets:write`\n\nApplies the single-job retry (`POST /v1/jobs/{id}/retry`) to each id (duplicates collapsed) and always answers\n200 with one outcome per id: `retried: true`, `retried: false` with a `note` (already queued, running,\nsucceeded or retried), or `retried: false` with an `error` (no such job, not retryable). `retried` counts the\nre-queued jobs. Each re-queued job emits `job.status` and is audited as `job.retry`. Platform tenants only.",
        "requestBody": {
          "required": true,
          "description": "JSON object, at most 64 KiB; unknown fields are rejected (400).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 200,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              },
              "example": {
                "ids": [
                  "01a10a77-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
                  "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                  "01a0fe02-3c4d-7e5f-8a6b-7c8d9e0f1a2b"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-job outcomes, in request order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "retried",
                    "results"
                  ],
                  "properties": {
                    "retried": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JobRetryOutcome"
                      }
                    }
                  }
                },
                "example": {
                  "retried": 1,
                  "results": [
                    {
                      "id": "01a10a77-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
                      "status": "queued",
                      "retried": true
                    },
                    {
                      "id": "01a0f9b4-7d6c-7b5a-8f4e-3d2c1b0a9f8e",
                      "status": "failed",
                      "retried": false,
                      "error": "not retryable: No recording for this window"
                    },
                    {
                      "id": "01a0fe02-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
                      "status": "succeeded",
                      "retried": false,
                      "note": "already succeeded"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` — the body is not valid JSON, has unknown fields or is larger than 64 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_error` — `ids` empty or more than 200",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/events/stream": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Server-sent events for the current tenant",
        "operationId": "eventStream",
        "description": "**Required scope:** `events:read`\n\n`text/event-stream` of the current tenant's domain and UI events. Frames are `id: <ULID>` / `event: <type>` /\n`data: <the event payload as JSON>`; `: connected` after the replay, a `: ping` comment every 15 s; after 1 h the\nserver sends `event: reconnect` (`{\"reason\":\"max_lifetime\"}`) and closes. Reconnect with `Last-Event-ID`: frames\nafter that id are replayed from a 5-minute ring (≤ 5000 per tenant); an id older than the ring yields\n`event: resync` (`{\"reason\":\"last_event_id_too_old\"}`) — refetch state. Slow clients may miss frames (they are\ndropped, not buffered). Types include `asset.status`, `asset.ready`, `asset.published`, `asset.failed`,\n`job.status`, `job.progress`, `clip.*`, `channel.*`, `notification`, `webhook.delivery`, `alert.*`. Session\ncookie (any member), or an API key with scope `events:read` (checked by the handler).",
        "parameters": [
          {
            "name": "Last-Event-ID",
            "in": "header",
            "description": "The `id` of the last frame received",
            "to resume after a reconnect": null,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The stream (stays open up to 1 h)",
            "headers": {
              "X-Accel-Buffering": {
                "schema": {
                  "type": "string"
                },
                "description": "no"
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "id: 01J8Z0K2R9X0Y7Q4M3N2P1V6WA\nevent: asset.status\ndata: {\"asset_id\":\"0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f\",\"status\":\"ready\"}\n\n: ping\n\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The event stream is not configured on this node (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "events:read"
      }
    },
    "/v1/invitations": {
      "get": {
        "tags": [
          "team"
        ],
        "summary": "Pending invitations of the current tenant (not accepted, not revoked, not expired)",
        "description": "**Required scope:** `team:manage`\n\nInvitations that can still be accepted, newest first. Accepted, revoked and expired invitations are not listed.\nThe accept link itself is never returned again (only its hash is stored); resend to get a new one.",
        "operationId": "listInvitations",
        "responses": {
          "200": {
            "description": "Pending invitations, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PendingInvitation"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "0199b4c1-7d20-7a8e-b1c2-3d4e5f607182",
                      "email": "noa@example.co.il",
                      "role": "editor",
                      "invited_by": "019286a2-4c33-7c4b-9a1b-3c5d7e9f1a2e",
                      "expires_at": "2026-10-13T08:00:00Z",
                      "created_at": "2026-10-06T08:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      },
      "post": {
        "tags": [
          "invitations"
        ],
        "summary": "Invite a user to the current tenant; the accept link is mailed to the invitee only",
        "description": "**Required scope:** `team:manage`\n\nCreates an invitation valid 7 days and mails the accept link (`<Studio>/invite/<token>`) to the invited\naddress. The link is never returned to the caller (security audit 2026-10-06 H1); a failed mail does not\nfail the call — resend it. The email is trimmed and lower-cased. Only an owner's console session may invite\nan `owner` (else 422; an API key never may).\n409 when the address is already a member of the tenant (any status). Nobody gets an account without\naccepting. Audited (`invitation.create`).",
        "operationId": "createInvitation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvitationInput"
              },
              "example": {
                "email": "noa@example.co.il",
                "role": "editor"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invitation created (valid 7 days); mailed when a mailer is configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitationCreated"
                },
                "example": {
                  "invitation": {
                    "id": "0199b4c1-7d20-7a8e-b1c2-3d4e5f607182",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "email": "noa@example.co.il",
                    "role": "editor",
                    "invited_by": "019286a2-4c33-7c4b-9a1b-3c5d7e9f1a2e",
                    "expires_at": "2026-10-13T08:00:00Z",
                    "accepted_at": null,
                    "revoked_at": null,
                    "created_at": "2026-10-06T08:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The address is already a member of this tenant (`conflict`; detail names role and status)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      }
    },
    "/v1/invitations/{id}": {
      "delete": {
        "tags": [
          "team"
        ],
        "summary": "Revoke a pending invitation (its link answers 410 from now on)",
        "description": "**Required scope:** `team:manage`\n\nRevokes a pending invitation of the current tenant. Only an owner's console session may revoke\nan invitation for the `owner` role (403; never an API key — security audit 2026-10-06 M3). 409 when the invitation was already accepted or revoked. Audited\n(`invitation.revoke`).",
        "operationId": "revokeInvitation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Invitation id (from GET /v1/invitations)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      }
    },
    "/v1/invitations/{id}/resend": {
      "post": {
        "tags": [
          "team"
        ],
        "summary": "Mail the invitation again with a NEW link (the previous link stops working) and 7 more days",
        "description": "**Required scope:** `team:manage`\n\nIssues a new token for a pending invitation, extends it to 7 days from now and mails the new link (when mail\nis configured); the link is never returned. The previous link stops working immediately. Same owner rule as\nrevoke (403); 409 when it is no longer pending. Audited (`invitation.resend`).",
        "operationId": "resendInvitation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Invitation id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Re-sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invitation"
                  ],
                  "properties": {
                    "invitation": {
                      "$ref": "#/components/schemas/PendingInvitation"
                    }
                  }
                },
                "example": {
                  "invitation": {
                    "id": "0199b4c1-7d20-7a8e-b1c2-3d4e5f607182",
                    "email": "noa@example.co.il",
                    "role": "editor",
                    "invited_by": "019286a2-4c33-7c4b-9a1b-3c5d7e9f1a2e",
                    "expires_at": "2026-10-13T10:30:00Z",
                    "created_at": "2026-10-06T08:00:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      }
    },
    "/v1/audit": {
      "get": {
        "tags": [
          "team"
        ],
        "summary": "The tenant's audit log, newest first",
        "description": "**Required scope:** `team:manage`\n\nThe tenant's audit log, newest first — Interhost operator actions on the tenant included (actor \"interhost:<email> as <tenant>\")\n\nRows of the current tenant only, newest first, keyset-paginated: pass the previous page's `next_before` as\n`before`; `next_before` is null when the page was not full. Actors look like `user:<email>`, `key:<prefix>`,\n`console` (sign-in events) or `interhost:<email> as <tenant>`. `actor` and `action` match as prefixes, `q` as a\ncase-insensitive substring of actor, action or target. `data` is the action's detail (always with `request_id`).",
        "operationId": "tenantAudit",
        "parameters": [
          {
            "name": "actor",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "actor prefix (user:, key:, interhost:)"
          },
          {
            "name": "action",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "action prefix, e.g. asset. or team."
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "substring of actor, action or target"
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "RFC 3339; rows at or after"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "RFC 3339; rows before"
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "keyset cursor (next_before of the previous page)"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of audit rows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "next_before"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AuditEntry"
                      }
                    },
                    "next_before": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "id to pass as before for the next page; null at the end"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": 184233,
                      "at": "2026-10-06T08:00:01Z",
                      "actor": "user:dana@example.co.il",
                      "tenant_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "action": "invitation.create",
                      "target": "noa@example.co.il",
                      "data": {
                        "role": "editor",
                        "invitation_id": "0199b4c1-7d20-7a8e-b1c2-3d4e5f607182",
                        "request_id": "0199b4c1-7cff-7d10-8e2a-0b1c2d3e4f50"
                      }
                    }
                  ],
                  "next_before": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      }
    },
    "/v1/users": {
      "get": {
        "tags": [
          "team"
        ],
        "summary": "Members of the current tenant (disabled members included)",
        "description": "**Required scope:** `team:manage`\n\nEvery membership of the current tenant, active and disabled, ordered by email. `you` marks the calling user\n(always false for an API key). Pending invitations are listed by `GET /v1/invitations`.",
        "operationId": "listUsers",
        "responses": {
          "200": {
            "description": "Members by email",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TeamMember"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "019286a2-4c33-7c4b-9a1b-3c5d7e9f1a2e",
                      "email": "dana@example.co.il",
                      "name": "Dana Levi",
                      "role": "owner",
                      "status": "active",
                      "last_login_at": "2026-10-06T07:45:12Z",
                      "joined_at": "2026-09-27T09:00:00Z",
                      "you": false
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      },
      "post": {
        "tags": [
          "team"
        ],
        "summary": "Invite by email (same as POST /v1/invitations — nobody gets an account without accepting the mail)",
        "description": "**Required scope:** `team:manage`\n\nInvite by email (same as POST /v1/invitations — nobody gets an account without accepting the mail); 409 for an existing member\n\nExactly `POST /v1/invitations`: creates a 7-day invitation and mails the link to the invitee (never returned).\nOnly an owner's console session may invite an owner (422). Audited (`invitation.create`).",
        "operationId": "createUser",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvitationInput"
              },
              "example": {
                "email": "noa@example.co.il",
                "role": "publisher"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invitation created and mailed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitationCreated"
                },
                "example": {
                  "invitation": {
                    "id": "0199b4c1-7d20-7a8e-b1c2-3d4e5f607182",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "email": "noa@example.co.il",
                    "role": "publisher",
                    "invited_by": null,
                    "expires_at": "2026-10-13T08:00:00Z",
                    "accepted_at": null,
                    "revoked_at": null,
                    "created_at": "2026-10-06T08:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      }
    },
    "/v1/users/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "patch": {
        "tags": [
          "team"
        ],
        "summary": "Change a member's role and/or status",
        "description": "**Required scope:** `team:manage`\n\nChange a member's role and/or status. Only an owner grants/changes/disables the owner role; the last active owner cannot be demoted or disabled; nobody disables themselves (409)\n\nGive `role`, `status` or both (422 when neither). Rules: only an owner's console session may grant, change or\ndisable the owner role (403; never an API key — security audit 2026-10-06 M3); the tenant must keep at least one active owner (409); nobody disables themselves\n(409). A disabled member keeps the membership but its tenant does not appear in their sessions. Audited\n(`team.role_change`, `team.disable`, `team.enable`). Returns the member after the change.",
        "operationId": "patchUser",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "minProperties": 1,
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "editor",
                      "publisher",
                      "engineer",
                      "admin",
                      "owner"
                    ]
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "disabled"
                    ]
                  }
                }
              },
              "example": {
                "role": "publisher"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamMember"
                },
                "example": {
                  "id": "0199b4c2-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
                  "email": "noa@example.co.il",
                  "name": "Noa Cohen",
                  "role": "publisher",
                  "status": "active",
                  "last_login_at": "2026-10-06T09:02:10Z",
                  "joined_at": "2026-10-06T08:20:00Z",
                  "you": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such user in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      },
      "delete": {
        "tags": [
          "team"
        ],
        "summary": "Remove the member from the tenant (the user account itself stays); the last owner cannot be removed",
        "description": "**Required scope:** `team:manage`\n\nDeletes the membership; the user account and its other memberships stay, and their open sessions lose this\ntenant. Removing an owner needs an owner's console session (403; never an API key); the last active owner cannot be removed (409).\nAudited (`team.remove`).",
        "operationId": "deleteUser",
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such user in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "team:manage"
      }
    },
    "/v1/notification-rules": {
      "get": {
        "tags": [
          "notifications"
        ],
        "summary": "Notification rules of the tenant, the subscribable events and whether Telegram is configured",
        "description": "**Required scope:** `notifications:manage`\n\nThe tenant's notification rules (email via the PMG relay, Telegram via the ViewStream tenant bot), the event\nkeys a rule may subscribe to (`events`) and `telegram`: whether the Telegram bot is configured on this\ndeployment. Needs `notifications:manage` (Admin and up).",
        "operationId": "listNotificationRules",
        "responses": {
          "200": {
            "description": "Rules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "events",
                    "telegram"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotificationRule"
                      }
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Subscribable event keys (besides `*`)"
                    },
                    "telegram": {
                      "type": "boolean",
                      "description": "The Telegram bot is configured"
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "0192a1e0-3f4a-7b5c-8d6e-7f8091a2b3c4",
                      "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                      "name": "NOC on-call",
                      "events": [
                        "feed.lost",
                        "feed.restored",
                        "ingest.failover"
                      ],
                      "channel": "telegram",
                      "recipients": [
                        "-1001234567890"
                      ],
                      "enabled": true,
                      "created_by": "01927e12-0a1b-7c2d-8e3f-405162738495",
                      "created_at": "2026-09-30T12:00:00Z",
                      "updated_at": "2026-09-30T12:00:00Z"
                    }
                  ],
                  "events": [
                    "feed.lost",
                    "feed.restored",
                    "ingest.failover",
                    "recording.redundancy",
                    "asset.failed",
                    "asset.ready",
                    "storage.warning",
                    "epg.delivery_failed"
                  ],
                  "telegram": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "notifications:manage"
      },
      "post": {
        "tags": [
          "notifications"
        ],
        "summary": "Create a rule (event → channel → recipients)",
        "description": "**Required scope:** `notifications:manage`\n\nCreate a rule (event → channel → recipients). Email recipients must be active members of the tenant; Telegram recipients are chat ids the ViewStream tenant bot is in\n\nA rule sends every matching notification (one inbox entry per event; a repeat of the same key for the same\nchannel or asset within 2 minutes is suppressed) to 1–10 recipients over one channel. `name` 1–100 characters; `events` from the keys of\nthe list response or `*`. `email`: each recipient must be an active member of the tenant (the relay is never an\nopen mailer; addresses are lower-cased). `telegram`: numeric chat ids (`-` prefix for groups) the bot is in.\nNew rules are enabled unless `enabled: false`. Audited as `notification_rule.create`.",
        "operationId": "createNotificationRule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NotificationRuleInput"
                  },
                  {
                    "required": [
                      "name",
                      "events",
                      "channel",
                      "recipients"
                    ]
                  }
                ]
              },
              "example": {
                "name": "NOC on-call",
                "events": [
                  "feed.lost",
                  "feed.restored"
                ],
                "channel": "telegram",
                "recipients": [
                  "-1001234567890"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationRule"
                },
                "example": {
                  "id": "0192a1e0-3f4a-7b5c-8d6e-7f8091a2b3c4",
                  "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                  "name": "NOC on-call",
                  "events": [
                    "feed.lost",
                    "feed.restored"
                  ],
                  "channel": "telegram",
                  "recipients": [
                    "-1001234567890"
                  ],
                  "enabled": true,
                  "created_by": "01927e12-0a1b-7c2d-8e3f-405162738495",
                  "created_at": "2026-10-06T07:40:00Z",
                  "updated_at": "2026-10-06T07:40:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntegrationBadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/notification-rules/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "patch": {
        "tags": [
          "notifications"
        ],
        "summary": "Change a rule",
        "description": "**Required scope:** `notifications:manage`\n\nPartial update: fields sent replace the stored ones (`events`, `recipients` as whole lists), then the merged\nrule is validated as on create — so changing `channel` needs matching `recipients`. Audited as\n`notification_rule.update`.",
        "operationId": "patchNotificationRule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NotificationRuleInput"
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationRule"
                },
                "example": {
                  "id": "0192a1e0-3f4a-7b5c-8d6e-7f8091a2b3c4",
                  "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                  "name": "NOC on-call",
                  "events": [
                    "feed.lost",
                    "feed.restored"
                  ],
                  "channel": "telegram",
                  "recipients": [
                    "-1001234567890"
                  ],
                  "enabled": false,
                  "created_by": "01927e12-0a1b-7c2d-8e3f-405162738495",
                  "created_at": "2026-10-06T07:40:00Z",
                  "updated_at": "2026-10-06T08:02:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntegrationBadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "notifications:manage"
      },
      "delete": {
        "tags": [
          "notifications"
        ],
        "summary": "Delete a rule",
        "description": "**Required scope:** `notifications:manage`\n\nDeletes the rule; past notifications and their send log stay. Audited as `notification_rule.delete`.",
        "operationId": "deleteNotificationRule",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/notification-rules/{id}/test": {
      "post": {
        "tags": [
          "notifications"
        ],
        "summary": "Send one clearly labelled TEST notification through this rule only (1 per rule per 30 s)",
        "description": "**Required scope:** `notifications:manage`\n\nRecords a `test` notification in the inbox and sends it synchronously to every recipient of this rule only\n(even when the rule is disabled), titled \"TEST — ViewStream notification test (<tenant>)\". Answers 200 with one\nsend result per recipient (`sent` or `failed` with the error). Limited to one test per rule every 30 s (429\nwith `Retry-After: 30`); 503 when notifications are not configured on the node. Audited as\n`notification_rule.test`.",
        "operationId": "testNotificationRule",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Notification rule id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per-recipient results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sends"
                  ],
                  "properties": {
                    "sends": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotificationSend"
                      }
                    }
                  }
                },
                "example": {
                  "sends": [
                    {
                      "id": "0192a1f2-6a7b-7c8d-9e0f-102132435465",
                      "notification_id": "0192a1f2-6a70-7b1c-8d2e-3f4051627384",
                      "rule_id": "0192a1e0-3f4a-7b5c-8d6e-7f8091a2b3c4",
                      "channel": "telegram",
                      "recipient": "-1001234567890",
                      "status": "sent",
                      "error": null,
                      "created_at": "2026-10-06T07:41:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "description": "A test of this rule was sent less than 30 s ago (`rate_limited`), or the per-key request rate was exceeded",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "description": "Notifications not configured on this node (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/notifications": {
      "get": {
        "tags": [
          "notifications"
        ],
        "summary": "The tenant's notification inbox (the Studio bell), newest first, with the caller's read state",
        "description": "**Required scope:** `events:read`\n\nNotifications of the tenant (feed lost/restored, ingest failover, recording redundancy, asset ready/failed,\nstorage warnings, EPG delivery failures, monitor alerts, rule tests), newest first, with `read` and the `unread`\ncount for the calling user. Read state is per user: an API key has none, so every item is unread for it. Needs\n`events:read` (every role).",
        "operationId": "listNotifications",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Items to return (1–200)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "unread",
            "in": "query",
            "description": "`1` or `true` = only items the caller has not read",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Notifications + unread count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "unread"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Notification"
                      }
                    },
                    "unread": {
                      "type": "integer",
                      "description": "Unread notifications of the caller (all of them",
                      "not only this page)": null
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "0192a1f0-1a2b-7c3d-8e4f-5061728394a5",
                      "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                      "event_type": "feed.lost",
                      "severity": "critical",
                      "title": "Feed lost on tv10 (enc-med1-a)",
                      "body": "The contribution feed of channel tv10 on encoder enc-med1-a is down; viewers see the slate.",
                      "data": {
                        "channel_id": "01927f00-1b2c-7d3e-8f40-516273849506",
                        "channel": "tv10",
                        "encoder": "enc-med1-a",
                        "state": "down",
                        "previous": "up",
                        "channel_state": "degraded"
                      },
                      "created_at": "2026-10-06T07:38:12Z",
                      "read": false
                    }
                  ],
                  "unread": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`limit` is not an integer in 1–200",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "events:read"
      }
    },
    "/v1/channels": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "List the tenant's channels (ingest secrets masked unless channels:operate)",
        "description": "**Required scope:** `channels:read`\n\nAll channels of the tenant that are not in the trash, with their reported state, per-encoder feed state and\nplayback URL templates. Not paginated (a tenant has a handful of channels). Without `channels:operate` every\nnon-boolean value in `ingest` is masked as `***`; the sealed SRT passphrase never appears (only\n`srt_passphrase_set`). `recording_status` is only on GET of one channel. CDN-only tenants get 403\n`feature_disabled`.",
        "operationId": "listChannels",
        "responses": {
          "200": {
            "description": "Channels",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Channel"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                      "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "slug": "main",
                      "title": "tv10 live",
                      "ladder": "tv10-redge",
                      "dvr_window_s": 7200,
                      "retention_days": 14,
                      "encoders": {
                        "a": "enc-1"
                      },
                      "state": "live",
                      "feed_state": {
                        "enc-1": {
                          "state": "up",
                          "at": "2026-10-06T07:46:12Z",
                          "detail": {
                            "note": "heartbeat",
                            "av": {
                              "rms_db": -22.7,
                              "frozen_s": 0,
                              "silence_s": 0
                            }
                          }
                        }
                      },
                      "deinterlace": "auto",
                      "ingest": {
                        "srt_passphrase_set": true,
                        "srt_passphrase_set_at": "2026-09-29T11:20:00Z"
                      },
                      "requested_state": null,
                      "playback": {
                        "live": "https://cdn.tv10-poc.vustream.net/live/tv10poc/main/master.m3u8",
                        "start_over": "https://cdn.tv10-poc.vustream.net/m/startover/main/{programme_id}/master.m3u8?c=tv10poc",
                        "catch_up": "https://cdn.tv10-poc.vustream.net/m/catchup/main/{start}/{end}/master.m3u8?c=tv10poc"
                      },
                      "created_at": "2026-09-01T10:00:00Z",
                      "updated_at": "2026-10-06T07:46:12Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Create a channel",
        "description": "**Required scope:** `channels:write`\n\nCreates a channel in state `stopped`. `slug` is required (2–63 chars of a-z 0-9 -, unique per tenant). Unset\nfields take the tenant defaults: `ladder` = the tenant's default ladder, `dvr_window_s` and `retention_days` =\nSettings → Defaults, `encoders` = {\"a\": \"enc-a\"}, `deinterlace` = auto. `state` and `policy_id` are ignored\nhere (use PATCH). The answer shows `ingest` unmasked. Body ≤ 64 KB; unknown fields → 400. Audited as\n`channel.create`. CDN-only tenants get 403 `feature_disabled`.",
        "operationId": "createChannel",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChannelInput"
              },
              "example": {
                "slug": "main",
                "title": "tv10 live",
                "ladder": "tv10-redge",
                "dvr_window_s": 7200,
                "retention_days": 14,
                "encoders": {
                  "a": "enc-1"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                },
                "example": {
                  "id": "01a1100e-2c41-7a3b-9f10-5d2e8c7b6a01",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "slug": "main",
                  "title": "tv10 live",
                  "ladder": "tv10-redge",
                  "dvr_window_s": 7200,
                  "retention_days": 14,
                  "encoders": {
                    "a": "enc-1"
                  },
                  "state": "stopped",
                  "feed_state": {},
                  "deinterlace": "auto",
                  "ingest": {},
                  "requested_state": null,
                  "playback": {
                    "live": "https://cdn.tv10-poc.vustream.net/live/tv10poc/main/master.m3u8",
                    "start_over": "https://cdn.tv10-poc.vustream.net/m/startover/main/{programme_id}/master.m3u8?c=tv10poc",
                    "catch_up": "https://cdn.tv10-poc.vustream.net/m/catchup/main/{start}/{end}/master.m3u8?c=tv10poc"
                  },
                  "created_at": "2026-10-06T08:00:00Z",
                  "updated_at": "2026-10-06T08:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 64 KB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "A channel with this slug exists for the tenant (also while a deleted one is still in the trash)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "One channel with state, feed state and playback URL templates",
        "description": "**Required scope:** `channels:read`\n\nThe channel as in the list, plus `recording_status` for a channel recorded by two recorders (the status the\norchestrator last stored, ≤ ~30 s old). Without `channels:operate` the non-boolean `ingest` values are masked as\n`***`. A channel in the trash, of another tenant or a malformed id answers 404.",
        "operationId": "getChannel",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                },
                "example": {
                  "id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "slug": "main",
                  "title": "Channel 14 live",
                  "ladder": "now14-540p50",
                  "dvr_window_s": 14400,
                  "retention_days": 14,
                  "encoders": {
                    "a": "enc-1",
                    "b": "enc-2"
                  },
                  "state": "live",
                  "feed_state": {
                    "enc-1": {
                      "state": "up",
                      "at": "2026-10-06T07:46:12Z",
                      "detail": {
                        "note": "heartbeat"
                      }
                    },
                    "enc-2": {
                      "state": "up",
                      "at": "2026-10-06T07:46:11Z",
                      "detail": {
                        "note": "heartbeat"
                      }
                    }
                  },
                  "deinterlace": "auto",
                  "ingest": {
                    "srt_passphrase_set": true,
                    "srt_passphrase_set_at": "2026-09-29T11:20:00Z"
                  },
                  "requested_state": "live",
                  "playback": {
                    "live": "https://cdn.now14-poc.vustream.net/live/now14poc/main/master.m3u8",
                    "start_over": "https://cdn.now14-poc.vustream.net/m/startover/main/{programme_id}/master.m3u8?c=now14poc",
                    "catch_up": "https://cdn.now14-poc.vustream.net/m/catchup/main/{start}/{end}/master.m3u8?c=now14poc"
                  },
                  "recording_status": {
                    "mode": "dual_same_source",
                    "status": "ok",
                    "serving_leg": 1,
                    "legs": [
                      {
                        "leg": 1,
                        "encoder": "enc-1",
                        "state": "recording",
                        "last_segment_at": "2026-10-06T07:46:10Z"
                      },
                      {
                        "leg": 2,
                        "encoder": "enc-2",
                        "state": "recording",
                        "last_segment_at": "2026-10-06T07:46:09Z"
                      }
                    ],
                    "changed_at": "2026-10-05T13:00:40Z",
                    "checked_at": "2026-10-06T07:46:00Z"
                  },
                  "created_at": "2026-09-01T10:00:00Z",
                  "updated_at": "2026-10-06T07:46:12Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      },
      "patch": {
        "tags": [
          "channels"
        ],
        "summary": "Update a channel; `state` requests live|stopped (confirmed by the encoder), `ingest` needs channels:operate",
        "description": "**Required scope:** `channels:write`\n\nPartial update: only the fields sent change (`slug` cannot change). `state` records the requested state\n(`requested_state`); the encoder starts or stops the channel and reports it in `state`. Changing `state` or\n`ingest` needs `channels:operate` (403 `insufficient_scope` otherwise); the sealed SRT passphrase is kept\nacross an `ingest` change. `policy_id` attaches a playback policy (null = inherit the tenant's) and needs\n`delivery:write`; that part is applied first, emits `policy.changed` and is audited as `policy.attach`.\nBody ≤ 64 KB; unknown fields → 400. Audited as `channel.update`. The answer shows `ingest` unmasked.",
        "operationId": "patchChannel",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChannelInput"
              },
              "example": {
                "state": "live",
                "dvr_window_s": 14400,
                "deinterlace": "auto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                },
                "example": {
                  "id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "slug": "main",
                  "title": "tv10 live",
                  "ladder": "tv10-redge",
                  "dvr_window_s": 14400,
                  "retention_days": 14,
                  "encoders": {
                    "a": "enc-1"
                  },
                  "state": "stopped",
                  "feed_state": {
                    "enc-1": {
                      "state": "down",
                      "at": "2026-10-06T08:01:00Z"
                    }
                  },
                  "deinterlace": "auto",
                  "ingest": {
                    "srt_passphrase_set": true,
                    "srt_passphrase_set_at": "2026-09-29T11:20:00Z"
                  },
                  "requested_state": "live",
                  "playback": {
                    "live": "https://cdn.tv10-poc.vustream.net/live/tv10poc/main/master.m3u8",
                    "start_over": "https://cdn.tv10-poc.vustream.net/m/startover/main/{programme_id}/master.m3u8?c=tv10poc",
                    "catch_up": "https://cdn.tv10-poc.vustream.net/m/catchup/main/{start}/{end}/master.m3u8?c=tv10poc"
                  },
                  "created_at": "2026-09-01T10:00:00Z",
                  "updated_at": "2026-10-06T08:01:05Z"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 64 KB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing channels:write, `state`/`ingest` without channels:operate, `policy_id` without delivery:write, or a CDN-only tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel (or, with `policy_id`, no such policy)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      },
      "delete": {
        "tags": [
          "channels"
        ],
        "summary": "Move a stopped channel to the trash",
        "description": "**Required scope:** `channels:write`\n\nOnly a stopped channel can be deleted (409 otherwise): no pending `live` request, no encoder reporting its feed\n`up`, and state `stopped` or an operator's `stopped` request (a channel whose feed was removed stays `degraded`\nwith its encoder `unknown`). The channel\ndisappears from the tenant's API, the encoders' channel lists and the EPG / live-to-VOD automation; its playback\npolicy is detached (emits `policy.changed`); its recordings stay until their retention. It is purged for good at `purge_at`\n(deleted_at + max(trash retention, retention_days + 1 day)); until then `POST /v1/channels/{id}/restore` brings\nit back (stopped, without a policy). The slug stays reserved until the purge. Audited as `channel.delete`.",
        "operationId": "deleteChannel",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "In the trash",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "slug",
                    "deleted_at",
                    "purge_at",
                    "policies_revoked",
                    "recordings_kept_until"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "deleted_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "purge_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "policies_revoked": {
                      "type": "integer",
                      "description": "Playback policy attachments removed (0 or 1)"
                    },
                    "recordings_kept_until": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When retention removes the newest recording (null = no recording)"
                    }
                  }
                },
                "example": {
                  "id": "01a0f3c2-7e10-7b44-8c2d-3f9a1b2c4d5e",
                  "slug": "s3test",
                  "deleted_at": "2026-10-06T08:10:00Z",
                  "purge_at": "2026-11-05T08:10:00Z",
                  "policies_revoked": 0,
                  "recordings_kept_until": "2026-10-01T07:26:44Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The channel is not stopped (stop it first and wait until no encoder reports its feed up)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/restore": {
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Take a channel out of the trash (stopped, without its old policy)",
        "description": "**Required scope:** `channels:write`\n\nRestores a channel deleted with `DELETE /v1/channels/{id}` before its `purge_at`. It comes back stopped and\nwithout the playback policy it had (the `channel.delete` audit entry names the old policy); re-attach one with\nPATCH `policy_id`. The answer masks `ingest` like a GET without channels:operate. Audited as `channel.restore`.",
        "operationId": "restoreChannel",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the trashed channel",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Restored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                },
                "example": {
                  "id": "01a0f3c2-7e10-7b44-8c2d-3f9a1b2c4d5e",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "slug": "s3test",
                  "title": null,
                  "ladder": "now14-540p50",
                  "dvr_window_s": 3600,
                  "retention_days": 1,
                  "encoders": {
                    "a": "enc-1"
                  },
                  "state": "stopped",
                  "feed_state": {
                    "enc-1": {
                      "state": "unknown",
                      "at": "2026-09-30T07:26:44Z"
                    }
                  },
                  "deinterlace": "auto",
                  "ingest": {},
                  "requested_state": null,
                  "playback": {
                    "live": "https://cdn.now14-poc.vustream.net/live/now14poc/s3test/master.m3u8",
                    "start_over": "https://cdn.now14-poc.vustream.net/m/startover/s3test/{programme_id}/master.m3u8?c=now14poc",
                    "catch_up": "https://cdn.now14-poc.vustream.net/m/catchup/s3test/{start}/{end}/master.m3u8?c=now14poc"
                  },
                  "created_at": "2026-09-29T09:00:00Z",
                  "updated_at": "2026-10-06T08:20:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel (or already purged)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The channel is not in the trash",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/programmes": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "Programmes overlapping [from, to) (default ±24 h)",
        "description": "**Required scope:** `channels:read`\n\nThe channel's guide: every programme that overlaps [from, to) (an open programme counts as running to `to`),\nordered by start. Not paginated. An unparsable `from`/`to` is ignored (the default is used). Excluded\nprogrammes stay in the list and carry `catchup_exclusion` (Studio shows the badge and hides start-over for\nthem). When programme boundaries are enabled each item also carries its effective playback window `bounds`\nand `adjusted_ms`.",
        "operationId": "listProgrammes",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "RFC 3339; default now − 24 h",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "RFC 3339; default now + 24 h",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Programmes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Programme"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01a1100e-744e-7d06-92f1-adf0be53ee01",
                      "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                      "start_at": "2026-10-06T06:30:00Z",
                      "end_at": "2026-10-06T07:00:00Z",
                      "title": "סוגרים שוק",
                      "source": "epg",
                      "external_id": "redge-81234567",
                      "meta": {
                        "lang": "he",
                        "category": "סוגרים שוק",
                        "image": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/epg-art/7313d1bfcbec4cc204257e11e94a0b87542c390e.jpg"
                      },
                      "created_at": "2026-10-05T00:00:12Z",
                      "bounds": {
                        "play_start": "2026-10-06T06:30:53.3Z",
                        "play_stop": "2026-10-06T07:00:41.3Z",
                        "bounds_status": "detected",
                        "bounds_method": "opener",
                        "bounds_confidence": 0.92,
                        "air_delay_ms": 41300
                      },
                      "adjusted_ms": 53300
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Bulk upsert programmes (JSON array, keyed by external_id or start_at) or import an XMLTV document",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nJSON body: 1–5000 programmes (body ≤ 8 MB). Each is matched by `external_id` when given, else by `start_at`\namong programmes without one, and updated or inserted. `source` defaults to `epg`. With EPG publishing on: new `manual` programmes without `external_id` are an editor's change — their `meta`\nis checked against EPGMeta, and they are staged (202) in review mode or published as a version (lock window\n→ 409 unless a publisher sends `confirm_lock=1`); SCTE-35 rows are written directly; other rows are an import\n(staged with 202 when the channel stages imports, else written and recorded as a version).\nWith `Content-Type: application/xml` (or `text/xml`) the body is an XMLTV document (≤ 10 MB). Programmes of\nthe selected XMLTV channel (`xmltv_channel`, else the channel's configured `xmltv_channel_id`, else the only\nchannel in the document) are upserted with source `epg`; `epg` programmes inside the imported window that the\ndocument no longer lists are deleted. Manual and SCTE-35 programmes are never touched.\nAudited as `programmes.upsert` / `programmes.import_xmltv`.",
        "operationId": "upsertProgrammes",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "xmltv_channel",
            "in": "query",
            "description": "XMLTV channel id to import (XMLTV body only)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "description": "Preferred title/description language (XMLTV body only, default he)",
            "schema": {
              "type": "string",
              "default": "he"
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "confirm a change inside the lock window (publishers only)",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "minItems": 1,
                "maxItems": 5000,
                "items": {
                  "$ref": "#/components/schemas/ProgrammeInput"
                }
              },
              "example": [
                {
                  "start_at": "2026-10-07T06:00:00Z",
                  "end_at": "2026-10-07T06:30:00Z",
                  "title": "פותחים שוק",
                  "external_id": "cms-p-20261007-0600",
                  "meta": {
                    "category": "כלכלה"
                  }
                }
              ]
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XMLTV document"
              },
              "example": "<tv><channel id=\"tv10\"/><programme start=\"20260928180000 +0300\" stop=\"20260928190000 +0300\" channel=\"tv10\"><title lang=\"he\">סוגרים שוק</title></programme></tv>"
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON body: the upserted programmes. XMLTV body: the import result.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Programme"
                          }
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/EPGImportResult"
                    }
                  ]
                },
                "example": {
                  "items": [
                    {
                      "id": "01a1120a-0b1c-7d2e-8f30-4a5b6c7d8e01",
                      "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                      "start_at": "2026-10-07T06:00:00Z",
                      "end_at": "2026-10-07T06:30:00Z",
                      "title": "פותחים שוק",
                      "source": "epg",
                      "external_id": "cms-p-20261007-0600",
                      "meta": {
                        "category": "כלכלה"
                      },
                      "created_at": "2026-10-06T08:30:00Z"
                    }
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Staged in the channel's EPG draft (review mode, or the channel stages imports)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGCommitResult"
                },
                "example": {
                  "staged": true,
                  "ops": 1
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field, a JSON body over 8 MB or an unreadable XML body",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`lock_window` — an editor's change touches the next hours of the guide; publishers repeat with ?confirm_lock=1; or `conflict` — the draft changed meanwhile",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "XMLTV document larger than 10 MB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Empty or > 5000 items, a missing start_at, end_at not after start_at, an unknown source, invalid editor metadata, or an XMLTV document that cannot be read / has several channels without `xmltv_channel`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/programmes/{pid}": {
      "patch": {
        "tags": [
          "channels"
        ],
        "summary": "Edit one programme (Studio EPG editor); an edited `epg` programme becomes `manual` so imports keep the edit",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nPartial edit of start, end, title and metadata (`open_end: true` clears the end). With EPG publishing on the\nedit goes through the channel's workflow: in review mode it is staged in the draft (202); in auto mode it is\npublished as a new guide version (200), and a change inside the lock window answers 409 `lock_window` unless a\npublisher (epg:publish or channels:write) repeats it with `confirm_lock=1`. Without the workflow the row is\nupdated directly and an `epg` programme is re-saved as `manual`. `meta` is checked against EPGMeta (unknown\nfields rejected). Body ≤ 64 KB. Audited as `programme.update`.",
        "operationId": "patchProgramme",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "pid",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "confirm a change inside the lock window (publishers only)",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "start_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "end_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "must be after start_at"
                  },
                  "open_end": {
                    "type": "boolean",
                    "description": "clear end_at"
                  },
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "meta": {
                    "$ref": "#/components/schemas/EPGMeta"
                  }
                }
              },
              "example": {
                "title": "סוגרים שוק — מהדורה מיוחדת",
                "meta": {
                  "description": "מהדורה מורחבת לרגל פתיחת המסחר בתל אביב",
                  "category": "כלכלה"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The programme (auto mode: published as a new version)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Programme"
                },
                "example": {
                  "id": "01a1100e-744e-7d06-92f1-adf0be53ee01",
                  "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                  "start_at": "2026-10-06T06:30:00Z",
                  "end_at": "2026-10-06T07:00:00Z",
                  "title": "סוגרים שוק — מהדורה מיוחדת",
                  "source": "manual",
                  "external_id": "redge-81234567",
                  "meta": {
                    "description": "מהדורה מורחבת לרגל פתיחת המסחר בתל אביב",
                    "category": "כלכלה"
                  },
                  "created_at": "2026-10-05T00:00:12Z"
                }
              }
            }
          },
          "202": {
            "description": "Review mode: staged in the draft",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGCommitResult"
                },
                "example": {
                  "staged": true,
                  "ops": 1
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 64 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`lock_window` — the change touches the next hours; publishers repeat with ?confirm_lock=1 (`conflict` when the draft changed meanwhile)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      },
      "delete": {
        "tags": [
          "channels"
        ],
        "summary": "Delete one programme",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nWith EPG publishing on, the delete goes through the channel's workflow: staged in the draft in review mode\n(202), published as a new guide version in auto mode (204; inside the lock window 409 `lock_window` unless a\npublisher repeats with `confirm_lock=1`). Without the workflow the row is deleted at once (204). An `epg`\nprogramme may come back with the next import of its source. Audited as `programme.delete`.",
        "operationId": "deleteProgramme",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "pid",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "confirm a change inside the lock window (publishers only)",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Review mode: staged in the draft",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGCommitResult"
                },
                "example": {
                  "staged": true,
                  "ops": 1
                }
              }
            }
          },
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`lock_window` (publishers repeat with ?confirm_lock=1) or `conflict` (the draft changed meanwhile)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/programmes/{pid}/bounds": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "A programme's effective playback window (programme-boundaries) with the detection detail",
        "description": "**Required scope:** `channels:read`\n\nPlayback (start-over, catch-up by programme, Sites, the player's previous-programme, live-to-VOD) uses the effective\nwindow, not the guide times: confirmed (an editor's cut or accept) > detected (the show's opener, or the learned\nbulletin length) > EPG + the channel's air delay. `unverified` keeps EPG + delay and may carry a `suggested` start\nin `detail`. `detail` is the detector's diagnostic record (matches, decision, expected start; `{}` before the\nfirst detection) — informative, its fields may change.",
        "operationId": "getProgrammeBounds",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "pid",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The effective window",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "programme_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "epg_start": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "epg_stop": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "bounds": {
                      "$ref": "#/components/schemas/ProgrammeBoundsEffective"
                    },
                    "adjusted_ms": {
                      "type": "integer",
                      "format": "int64",
                      "description": "play_start − epg_start"
                    },
                    "detail": {
                      "type": "object",
                      "description": "detector diagnostics (may include `suggested`)"
                    }
                  }
                },
                "example": {
                  "programme_id": "01a1100e-744e-7d06-92f1-adf0be53ee01",
                  "epg_start": "2026-10-06T06:35:00Z",
                  "epg_stop": "2026-10-06T07:00:00Z",
                  "bounds": {
                    "play_start": "2026-10-06T06:34:12Z",
                    "play_stop": "2026-10-06T07:02:11.3Z",
                    "bounds_status": "unverified",
                    "bounds_method": "offset",
                    "bounds_confidence": 0.3,
                    "air_delay_ms": 12000
                  },
                  "adjusted_ms": -48000,
                  "detail": {
                    "v2": true,
                    "algo": 5,
                    "matches": [
                      {
                        "at": "2026-10-06T06:38:22.4Z",
                        "ber": 0.38,
                        "role": "opener",
                        "margin": 0.002,
                        "marker_id": "01a0f27b-27dc-7552-9b6d-f8d3d9faf0a2"
                      }
                    ],
                    "decision": {
                      "apply": false,
                      "start": "2026-10-06T06:35:12Z",
                      "method": "offset",
                      "status": "unverified",
                      "confidence": 0.3
                    },
                    "expected": "2026-10-06T06:35:12Z",
                    "title_mismatch": null
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "Programme boundaries are not enabled on this control plane",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Accept, set (timeline cut) or reset a programme's playback window; a confirmed start teaches the show's opener",
        "description": "**Required scope:** `channels:write`\n\n`accept` confirms `start_at` if given, else the stored detection suggestion, else the current effective start;\n`set` confirms explicit recording-timeline times (`start_at` required; catch-up Edit & cut); `reset` goes back\nto EPG + air delay and detects again. A confirmed start must be within ±30 min of the guide start and `end_at`\nafter it; it becomes status `confirmed`, changes the start-over URL revision and feeds learning from\ncorrections (the show's opener, the bulletin length). Body ≤ 8 KB. Audited as `programme.bounds`.",
        "operationId": "postProgrammeBounds",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "pid",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "action"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "accept",
                      "set",
                      "reset"
                    ]
                  },
                  "start_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "recording-timeline time (±30 min of the guide start); required for set; accept without it uses the suggestion"
                  },
                  "end_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "accept / set: confirmed end (after start_at); omitted = keep the open / derived end"
                  }
                }
              },
              "example": {
                "action": "set",
                "start_at": "2026-10-06T06:34:58Z",
                "end_at": "2026-10-06T07:01:40Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new effective window",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "programme_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "bounds": {
                      "$ref": "#/components/schemas/ProgrammeBoundsEffective"
                    },
                    "adjusted_ms": {
                      "type": "integer",
                      "format": "int64"
                    }
                  }
                },
                "example": {
                  "programme_id": "01a1100e-744e-7d06-92f1-adf0be53ee01",
                  "bounds": {
                    "play_start": "2026-10-06T06:34:58Z",
                    "play_stop": "2026-10-06T07:01:40Z",
                    "bounds_status": "confirmed",
                    "bounds_method": "confirmed",
                    "air_delay_ms": 12000
                  },
                  "adjusted_ms": -2000
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 8 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Unknown action, set without start_at, end_at not after the start, or a start more than 30 min from the guide start",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "Programme boundaries are not enabled on this control plane",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/bounds": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "The channel's programme-boundary settings (air delay, detection on/off) and its boundary markers",
        "description": "**Required scope:** `channels:read`\n\n`settings` holds zero values until first saved. `markers` lists every boundary marker of the channel (show\nopeners, the bulletin sting, visual idents), enabled or not, with their hit statistics.",
        "operationId": "getChannelBounds",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Settings and markers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "settings": {
                      "$ref": "#/components/schemas/BoundsChannelSettings"
                    },
                    "markers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BoundsMarker"
                      }
                    }
                  }
                },
                "example": {
                  "settings": {
                    "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                    "enabled": true,
                    "air_delay_ms": 41300,
                    "air_delay_auto": false,
                    "early_margin_ms": 90000
                  },
                  "markers": [
                    {
                      "id": "01a0ed79-4f14-7e59-b281-7317275183c9",
                      "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                      "series_id": "01a0e1d0-1111-7222-8333-944455566677",
                      "role": "opener",
                      "label": "פתיח — סוגרים שוק",
                      "start_offset_ms": 0,
                      "offsets": [
                        0,
                        120,
                        -80
                      ],
                      "hits": 37,
                      "misses": 2,
                      "enabled": true,
                      "created_at": "2026-09-29T10:12:00Z",
                      "duration_ms": 8000,
                      "origin": "manual",
                      "wrong_starts": 0
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "Programme boundaries are not enabled on this control plane",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "channels"
        ],
        "summary": "Update the channel's programme-boundary settings",
        "description": "**Required scope:** `channels:write`\n\nPartial update over the stored settings: only the fields sent change. `air_delay_ms` must be within ±10 min and\n`early_margin_ms` within 0..10 min (422 otherwise). Takes effect for the next detection and for every\nunverified / EPG window at once. Body ≤ 16 KB. Audited as `channel.bounds`.",
        "operationId": "putChannelBounds",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "air_delay_ms": {
                    "type": "integer",
                    "minimum": -600000,
                    "maximum": 600000,
                    "description": "the source's delay behind real air time"
                  },
                  "air_delay_auto": {
                    "type": "boolean"
                  },
                  "clock": {
                    "type": "object",
                    "description": "on-air clock for OCR calibration: {crop:[x,y,w,h], rendition, format}"
                  },
                  "early_margin_ms": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 600000,
                    "default": 90000,
                    "description": "an unverified start plays this much before the guide time + delay, and its end this much after (never cut a programme)"
                  }
                }
              },
              "example": {
                "enabled": true,
                "air_delay_ms": 41300,
                "early_margin_ms": 90000
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundsChannelSettings"
                },
                "example": {
                  "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                  "enabled": true,
                  "air_delay_ms": 41300,
                  "air_delay_auto": false,
                  "early_margin_ms": 90000
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 16 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "Programme boundaries are not enabled on this control plane",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/bounds/markers": {
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Cut a boundary marker out of the recording (a show opener, the bulletin sting, or a visual channel ident)",
        "description": "**Required scope:** `channels:write`\n\nCut a boundary marker out of the recording (a show opener, the bulletin sting, or a visual channel ident); asynchronous\n\nQueues a `programme_bounds` learn job that fingerprints `duration_ms` of the recording from `at` (audio; an\n`ident` is matched on 1 fps frames) and stores it as a marker of the channel; follow up with\nGET /v1/channels/{id}/bounds. `at` must be a recorded time inside the retention window and at least 30 s ago.\nAn `opener` needs `series_id`. Body ≤ 8 KB. Audited as `channel.bounds_marker_learn`.",
        "operationId": "postBoundsMarker",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "role",
                  "at"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "opener",
                      "bulletin",
                      "ident"
                    ],
                    "description": "ident: matched on the picture (1 fps frames), not the audio"
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "recording-timeline time of the marker's first sound (ident: first frame)"
                  },
                  "duration_ms": {
                    "type": "integer",
                    "minimum": 2000,
                    "maximum": 20000,
                    "default": 8000
                  },
                  "series_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "required for an opener"
                  },
                  "start_offset_ms": {
                    "type": "integer",
                    "minimum": -600000,
                    "maximum": 600000,
                    "description": "the programme starts this long after the marker: bulletin = its length (initial estimate); opener = negative for a cold open before it; ident = its duration − 1 s"
                  },
                  "label": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "role": "bulletin",
                "at": "2026-09-29T09:01:13Z",
                "duration_ms": 6000,
                "start_offset_ms": 235000,
                "label": "מבזק כלכלה"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "queued": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "queued": true
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 8 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "Programme boundaries are not enabled on this control plane",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/bounds/markers/{mid}": {
      "patch": {
        "tags": [
          "channels"
        ],
        "summary": "Enable or disable a boundary marker (a show opener or the bulletin sting)",
        "description": "**Required scope:** `channels:write`\n\nA disabled marker is no longer used by detection (existing windows stay until re-detected). An absent\n`enabled` counts as false. Audited as `channel.bounds_marker`.",
        "operationId": "patchBoundsMarker",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "description": "Marker id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Updated"
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 4 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel or marker",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "Programme boundaries are not enabled on this control plane",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/bounds/learning": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "Learning from corrections (Studio \"למידה מתיקונים\")",
        "description": "**Required scope:** `channels:read`\n\nLearning from corrections (Studio \"למידה מתיקונים\") — corrections, frame truths, detection accuracy, last training, learned / down-weighted markers\n\nCounts are zero and `markers` empty until the learning schema is migrated. `active` / `collecting` reflect the\ncontrol plane's switches; `enabled` is this channel's opt-out.",
        "operationId": "getBoundsLearning",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Learning status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundsLearningStatus"
                },
                "example": {
                  "enabled": true,
                  "active": true,
                  "collecting": true,
                  "corrections": 84,
                  "moved": 23,
                  "frame_truths": 40,
                  "bundles_ready": 81,
                  "last_corrected": "2026-10-05T19:12:44Z",
                  "accuracy": {
                    "checked": 40,
                    "within_5s": 31,
                    "within_60s": 38,
                    "late_over_5s": 4,
                    "median_abs_s": 2.1
                  },
                  "train_min": 20,
                  "retention_days": 180,
                  "markers": [
                    {
                      "id": "01a0ef02-082c-7823-bd4a-c4a5a57c808b",
                      "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                      "role": "opener",
                      "start_offset_ms": -42000,
                      "offsets": [
                        -41000,
                        -43000
                      ],
                      "hits": 12,
                      "misses": 1,
                      "enabled": true,
                      "created_at": "2026-09-30T08:00:00Z",
                      "duration_ms": 8000,
                      "origin": "correction",
                      "wrong_starts": 0,
                      "what": "cold_open"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "Programme boundaries are not enabled on this control plane",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "channels"
        ],
        "summary": "Opt the channel in or out of learning from corrections…",
        "description": "**Required scope:** `channels:write`\n\nOpt the channel in or out of learning from corrections (opting out stops recording and learning at once; purge also deletes the channel's examples and feature bundles)\n\n`enabled` is required; `purge: true` is only allowed with `enabled: false` and deletes the channel's recorded\ncorrections and their feature bundles now. 422 while the learning schema is not migrated. Answers the new\nlearning status. Body ≤ 4 KB. Audited as `channel.bounds_learning`.",
        "operationId": "putBoundsLearning",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "purge": {
                    "type": "boolean",
                    "default": false,
                    "description": "with enabled=false: delete the channel's recorded corrections and bundles now"
                  }
                }
              },
              "example": {
                "enabled": false,
                "purge": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Learning status after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundsLearningStatus"
                },
                "example": {
                  "enabled": false,
                  "active": true,
                  "collecting": true,
                  "corrections": 0,
                  "moved": 0,
                  "frame_truths": 0,
                  "bundles_ready": 0,
                  "train_min": 20,
                  "retention_days": 180,
                  "markers": []
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 4 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "Programme boundaries are not enabled on this control plane",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/recording/timeline": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "Recorded spans, programmes with first-frame thumbnails, the live poster and the snapshot URL template",
        "description": "**Required scope:** `channels:read`\n\nFor the window [from, to] (default the last 24 h; at most 8 days): the recorded stretches (from the first\nrecorded rendition among 540p, 720p, 360p, 480p, 1080p, 240p, 180p; gaps under 3 s joined), the programmes\noverlapping the window with the first recorder snapshot at their start, the live poster and a URL template for\nany snapshot (one every `thumb_interval_s` seconds). Use it to draw a scrubbable recording timeline.\n`Cache-Control: no-store`.",
        "operationId": "getRecordingTimeline",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "RFC 3339 or epoch ms; default to − 24 h (unparsable = default)"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "RFC 3339 or epoch ms; default now; at most 8 days after from"
          }
        ],
        "responses": {
          "200": {
            "description": "Timeline",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingTimeline"
                },
                "example": {
                  "from": "2026-09-28T08:00:00Z",
                  "to": "2026-09-28T10:00:00Z",
                  "spans": [
                    {
                      "from": "2026-09-28T08:00:00Z",
                      "to": "2026-09-28T10:00:00Z"
                    }
                  ],
                  "programmes": [
                    {
                      "id": "01a0e5f1-0000-7000-8000-000000000001",
                      "start": "2026-09-28T09:00:00Z",
                      "stop": "2026-09-28T10:00:00Z",
                      "title": "פותחים שוק",
                      "thumbnail": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/thumbs/20260928/0900/1790586000.jpg"
                    }
                  ],
                  "poster": "https://cdn.tv10-poc.vustream.net/live/tv10poc/main/poster.jpg",
                  "thumb_url_template": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/thumbs/{yyyymmdd}/{hhmm}/{epoch_s}.jpg",
                  "thumb_interval_s": 10
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "from is not before to, or they are more than 8 days apart",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/recording/thumb": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "Redirect (302) to the recorder snapshot nearest `at` (±60 s)",
        "description": "**Required scope:** `channels:read`\n\nLooks for an existing recorder snapshot in 10 s steps around `at` (up to 60 s either way, never in the future)\nand redirects to it on the tenant CDN hostname. The redirect may be cached privately for 5 minutes.",
        "operationId": "getRecordingThumb",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "at",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "RFC 3339 or epoch ms",
            "example": "2026-10-06T06:31:00Z"
          }
        ],
        "responses": {
          "302": {
            "description": "Location = the snapshot JPEG on the tenant CDN hostname",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                },
                "example": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/thumbs/20261006/0631/1791181860.jpg"
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "example": "private, max-age=300"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel, or no snapshot within 60 s of `at`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`at` is missing or neither RFC 3339 nor epoch milliseconds",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/catchup": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "Catch-up programme list",
        "description": "**Required scope:** `channels:read`\n\nCatch-up programme list — programmes of the window, newest first, with first-frame thumbnail, recorded coverage and VOD status\n\nProgrammes overlapping [from, to] (default the last 24 h; `to` is capped at now and `from` clipped to the retention window; at most 8 days), newest first, not paginated. status is `not_recorded` | `partial` (an ended programme with some but < 95 % recorded; it plays from its first recorded instant, live-to-VOD does not publish it) | `recorded` (≥ 95 % of an ended programme is recorded, or an airing one has any recording) | `publishing` | `published` | `failed`; `vod.asset_id` links the published asset. Coverage, duration and the thumbnail follow the programme's effective playback window (programme boundaries) when known; the thumbnail is the editor's / automatic poster when one exists. Programmes excluded from catch-up are left out and counted in `excluded_hidden`. Play a programme with the open-ended catch-up manifest `/m/catchup/{channel}/{start}/live/master.m3u8` so playback continues into the next programmes and live. `Cache-Control: no-store`.",
        "operationId": "getCatchup",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "RFC 3339 or epoch ms; default to − 24 h, clipped to the retention window"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "RFC 3339 or epoch ms; default and maximum now"
          }
        ],
        "responses": {
          "200": {
            "description": "Programmes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "retention_from": {
                      "type": "string",
                      "format": "date-time",
                      "description": "oldest instant still recorded under the channel's retention"
                    },
                    "excluded_hidden": {
                      "type": "integer",
                      "description": "Programmes of the window left out because they are excluded from catch-up (GET /v1/tenant/catchup-exclusions/programmes lists them)"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatchupItem"
                      }
                    }
                  }
                },
                "example": {
                  "from": "2026-10-05T08:00:00Z",
                  "to": "2026-10-06T08:00:00Z",
                  "retention_from": "2026-09-22T08:00:00Z",
                  "excluded_hidden": 1,
                  "items": [
                    {
                      "id": "01a1100e-744e-7d06-92f1-adf0be53ee01",
                      "start_at": "2026-10-06T06:30:00Z",
                      "end_at": "2026-10-06T07:00:00Z",
                      "title": "סוגרים שוק",
                      "category": "כלכלה",
                      "source": "epg",
                      "duration_s": 1788,
                      "thumbnail": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/thumbs/20261006/0631/1791181860.jpg",
                      "airing": false,
                      "coverage": 1,
                      "status": "published",
                      "vod": {
                        "asset_id": "01a11050-3c2d-7e1f-8a9b-0c1d2e3f4a5b",
                        "status": "published",
                        "trigger": "rule",
                        "start_at": "2026-10-06T06:30:53.3Z",
                        "end_at": "2026-10-06T07:00:41.3Z"
                      },
                      "play_start": "2026-10-06T06:30:53.3Z",
                      "play_end": "2026-10-06T07:00:41.3Z",
                      "bounds_status": "detected",
                      "bounds_method": "opener",
                      "adjusted_ms": 53300
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "from is not before to, or they are more than 8 days apart",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/catchup/search": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "Search the channel's catch-up programmes over the whole retention window",
        "description": "**Required scope:** `channels:read`\n\nProgrammes of the retention window whose title, EPG description or content summary (summary text, presenters, topics) contains `q`, case-insensitively (Hebrew as typed) — and programmes whose Hebrew subtitles contain it — newest first, at most `limit`. Items have the shape of GET /v1/channels/{id}/catchup plus `match`; a subtitle hit adds `moments` (up to 3: seconds from the programme's play start — the catch-up player's position — the line, and the highlighted span), and a programme found only in its subtitles has `match.field` = `transcript`. Subtitles are matched Hebrew-normalised (niqqud and quotes ignored, final letters folded, a word inside a longer one found: \"ריבית\" finds \"והריבית\"); the index is filled by the subtitles sweep within minutes of a new track. `transcript=false` searches the metadata only. Excluded programmes (catch-up exclusions) are left out. `Cache-Control: no-store`.",
        "operationId": "searchCatchup",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 100
            },
            "description": "2–100 characters after trimming"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "most results returned",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 50
            }
          },
          {
            "name": "transcript",
            "in": "query",
            "description": "false = do not search the subtitles",
            "schema": {
              "type": "boolean",
              "default": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching programmes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "q": {
                      "type": "string",
                      "description": "the query as searched (trimmed",
                      "inner whitespace collapsed)": null
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "more programmes matched than `limit`"
                    },
                    "retention_from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/CatchupItem"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "match": {
                                "$ref": "#/components/schemas/SearchMatch"
                              },
                              "moments": {
                                "type": "array",
                                "maxItems": 3,
                                "items": {
                                  "$ref": "#/components/schemas/TranscriptMoment"
                                },
                                "description": "v3: where in the programme q was said"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                },
                "example": {
                  "q": "אליפות אירופה",
                  "truncated": false,
                  "retention_from": "2026-09-21T10:00:00Z",
                  "items": [
                    {
                      "id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                      "start_at": "2026-10-04T17:00:00Z",
                      "end_at": "2026-10-04T18:00:00Z",
                      "title": "חמש",
                      "source": "epg",
                      "duration_s": 3600,
                      "airing": false,
                      "coverage": 1,
                      "status": "recorded",
                      "vod": null,
                      "adjusted_ms": 0,
                      "match": {
                        "field": "summary",
                        "snippet": "…דנו בסיכויי הנבחרת להעפיל לאליפות אירופה 2028, בלחץ הפוליטי…"
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "`q` shorter than 2 or longer than 100 characters after trimming, or `limit` outside 1..50",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Search is not available on this store (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/programmes/{pid}/publish-vod": {
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Publish a programme as a VOD asset",
        "description": "**Required scope:** `channels:read`\n\nNeeds `channels:read` and `assets:write` (plus `assets:publish` when the asset is to be published). Finalises [start_at, end_at) frame-exactly from the recording into vod/<tenant>/<asset>/ (boundary GOPs re-encoded, the rest copied); poster = the first frame; title/description/category from the programme. Without start_at / end_at the programme's effective playback window is used (programme boundaries, else the guide times); trimmed bounds are stored as the programme's confirmed boundaries (without boundaries: saved on the programme, which becomes manual). The programme must have ended, lie inside the retention window, be ≥ 95 % recorded, not be excluded from catch-up and not have VOD rights withheld (422 otherwise). 409 when the programme is publishing or published unless `replace: true` (a new asset; the old one stays in the Library); a published asset that was deleted from the Library may be published again. Answers 202 with the new asset (status `packaging`) and its `clip_finalize` job — follow GET /v1/jobs/{id} or the asset. The asset emits asset.ready (and asset.published) like an upload. Body ≤ 16 KB. Audited as `programme.publish_vod`.",
        "operationId": "publishProgrammeVOD",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "pid",
            "in": "path",
            "required": true,
            "description": "Programme id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "start_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "trimmed start on the recording timeline"
                  },
                  "end_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "trimmed end; required while the programme is open"
                  },
                  "collection_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "section id, or auto_by_programme_title (a section named after the programme)"
                  },
                  "publish": {
                    "type": "boolean",
                    "description": "default: the tenant auto_publish; true needs assets:publish"
                  },
                  "replace": {
                    "type": "boolean",
                    "default": false,
                    "description": "publish again although a VOD exists (a new asset)"
                  }
                }
              },
              "example": {
                "start_at": "2026-10-06T06:30:53Z",
                "end_at": "2026-10-06T07:00:41Z",
                "collection_id": "auto_by_programme_title",
                "publish": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Publishing (asset in packaging, clip_finalize job queued)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "asset_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "job_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "publishing"
                      ]
                    },
                    "coverage": {
                      "type": "number",
                      "description": "recorded share of the range (0..1)"
                    },
                    "programme_vod": {
                      "$ref": "#/components/schemas/ProgrammeVOD"
                    }
                  }
                },
                "example": {
                  "asset_id": "01a11280-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
                  "job_id": "01a11280-1a2c-7d4e-9f50-6b7c8d9e0f1a",
                  "status": "publishing",
                  "coverage": 1,
                  "programme_vod": {
                    "id": "01a11280-19ff-7a00-8b11-2c3d4e5f6a7b",
                    "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "programme_id": "01a1100e-744e-7d06-92f1-adf0be53ee01",
                    "programme_start": "2026-10-06T06:30:00Z",
                    "start_at": "2026-10-06T06:30:53Z",
                    "end_at": "2026-10-06T07:00:41Z",
                    "title": "סוגרים שוק",
                    "status": "publishing",
                    "asset_id": "01a11280-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
                    "job_id": "01a11280-1a2c-7d4e-9f50-6b7c8d9e0f1a",
                    "collection_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                    "publish": true,
                    "trigger": "manual",
                    "error": null,
                    "created_at": "2026-10-06T08:40:00Z",
                    "updated_at": "2026-10-06T08:40:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 16 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing channels:read or assets:write, `publish` (explicit or the tenant default) without assets:publish, or a CDN-only tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel or programme",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Already publishing / published. Unlike other errors this body is sent as `application/json` with `type: conflict` (no URL prefix, no request_id) and also carries `programme_vod`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "conflict"
                      ]
                    },
                    "title": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer",
                      "enum": [
                        409
                      ]
                    },
                    "detail": {
                      "type": "string"
                    },
                    "programme_vod": {
                      "$ref": "#/components/schemas/ProgrammeVOD"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Excluded from catch-up, still open without end_at, end_at not after start_at, not ended yet, outside retention, VOD rights withheld, under 95 % recorded, or an invalid collection",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Recordings are not configured on this control plane (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/programmes/publish-vod": {
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Publish every ended programme of a day or window (or the listed ones) as VOD",
        "description": "**Required scope:** `channels:read`\n\nNeeds `channels:read` and `assets:write` (plus `assets:publish` when publishing). The window is `date` (a calendar day in `tz`, default Asia/Jerusalem) or `from` + `to` (at most 48 h). Every programme overlapping it — or only those in `programme_ids` (\"Publish selected\") — is published with its guide times like POST …/programmes/{pid}/publish-vod; programmes still airing, already published / publishing, under-recorded, excluded or otherwise refused are listed in `skipped` with the reason instead of failing the call. Each created asset gets its own `clip_finalize` job. Body ≤ 64 KB. Audited as `programme.publish_vod_bulk`.",
        "operationId": "bulkPublishProgrammeVOD",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "date": {
                    "type": "string",
                    "format": "date",
                    "description": "YYYY-MM-DD in tz (takes precedence over from/to)"
                  },
                  "tz": {
                    "type": "string",
                    "default": "Asia/Jerusalem",
                    "description": "IANA time zone of `date`"
                  },
                  "from": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "to": {
                    "type": "string",
                    "format": "date-time",
                    "description": "at most 48 h after from"
                  },
                  "programme_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "only these programmes of the window"
                  },
                  "collection_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "section id, or auto_by_programme_title"
                  },
                  "publish": {
                    "type": "boolean",
                    "description": "default: the tenant auto_publish; true needs assets:publish"
                  }
                }
              },
              "example": {
                "date": "2026-10-05",
                "tz": "Asia/Jerusalem",
                "collection_id": "auto_by_programme_title",
                "publish": false
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Created and skipped programmes (with the reason)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "created": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "programme_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "asset_id": {
                            "type": "string",
                            "format": "uuid"
                          }
                        }
                      }
                    },
                    "skipped": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "programme_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "reason": {
                            "type": "string",
                            "example": "already published"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "from": "2026-10-04T21:00:00Z",
                  "to": "2026-10-05T21:00:00Z",
                  "created": [
                    {
                      "programme_id": "01a10a10-1111-7222-8333-944455566601",
                      "title": "פותחים שוק",
                      "asset_id": "01a11290-2b3c-7d4e-8f50-6a7b8c9d0e1f"
                    }
                  ],
                  "skipped": [
                    {
                      "programme_id": "01a10a10-1111-7222-8333-944455566602",
                      "title": "סוגרים שוק",
                      "reason": "already published"
                    },
                    {
                      "programme_id": "01a10a10-1111-7222-8333-944455566603",
                      "title": "מדברים נדל\"ן",
                      "reason": "still airing"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 64 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing channels:read or assets:write, publishing without assets:publish, or a CDN-only tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Neither date nor from+to, an invalid date or tz, or a window that is empty or longer than 48 h",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Recordings are not configured on this control plane (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/live-to-vod": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "The channel's live-to-VOD rule (disabled unless set)",
        "description": "**Required scope:** `channels:read`\n\nA channel that never saved a rule answers the defaults (disabled, min 300 s, section by programme title, manual publish, 120 s delay).",
        "operationId": "getLiveToVOD",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rule",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LiveToVODRule"
                },
                "example": {
                  "enabled": true,
                  "min_duration_s": 600,
                  "categories": [
                    "כלכלה"
                  ],
                  "title_pattern": "",
                  "collection": "auto_by_programme_title",
                  "publish": "manual",
                  "delay_s": 120
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "channels"
        ],
        "summary": "Replace the channel's live-to-VOD rule (the orchestrator publishes matching programmes delay_s after they end)",
        "description": "**Required scope:** `channels:write`\n\nNeeds `channels:write` and `assets:write`; `publish: auto` also needs `assets:publish`. Replaces the whole rule:\nfields left out take the defaults (see LiveToVODRule); `delay_s: 0` is stored as 120. `collection` must be\nempty, `auto_by_programme_title` or a section of this tenant. While enabled, the orchestrator publishes every\nended programme that matches (duration, categories, title pattern) `delay_s` after its end, like\npublish-vod with trigger `rule`. Body ≤ 16 KB. Audited as `channel.live_to_vod`.",
        "operationId": "putLiveToVOD",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LiveToVODRule"
              },
              "example": {
                "enabled": true,
                "min_duration_s": 600,
                "categories": [
                  "כלכלה"
                ],
                "title_pattern": "^(פותחים|סוגרים) שוק",
                "collection": "auto_by_programme_title",
                "publish": "auto",
                "delay_s": 300
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved rule",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LiveToVODRule"
                },
                "example": {
                  "enabled": true,
                  "min_duration_s": 600,
                  "categories": [
                    "כלכלה"
                  ],
                  "title_pattern": "^(פותחים|סוגרים) שוק",
                  "collection": "auto_by_programme_title",
                  "publish": "auto",
                  "delay_s": 300
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 16 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing channels:write or assets:write, `publish: auto` without assets:publish, or a CDN-only tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "A value out of range, an invalid regular expression or an unknown section",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/epg": {
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "EPG import settings, last-run status and the public export links of a channel",
        "description": "**Required scope:** `channels:read`\n\nThe scheduled EPG source of the channel (`config`, defaults when never set), the importer's last run\n(`status`) and the public XMLTV / JSON export URLs of this channel and of the whole tenant.",
        "operationId": "getChannelEPG",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "EPG settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelEPG"
                },
                "example": {
                  "config": {
                    "enabled": true,
                    "source_url": "https://epg.example-broadcaster.co.il/xmltv/tv10.xml",
                    "format": "xmltv",
                    "xmltv_channel_id": "tv10",
                    "interval_min": 60,
                    "daily_at": "00:00",
                    "days_ahead": 3,
                    "lang": "he"
                  },
                  "status": {
                    "last_run_at": "2026-10-06T08:00:02Z",
                    "last_ok_at": "2026-10-06T08:00:02Z",
                    "result": {
                      "source": "schedule",
                      "created": 4,
                      "updated": 61,
                      "deleted": 1,
                      "skipped": 0,
                      "errors": [],
                      "from": "2026-10-06T00:00:00+03:00",
                      "to": "2026-10-09T00:00:00+03:00"
                    }
                  },
                  "export": {
                    "xmltv": "https://api.viewstream.co.il/epg/tv10poc/main.xml",
                    "json": "https://api.viewstream.co.il/epg/tv10poc/main.json",
                    "tenant_xmltv": "https://api.viewstream.co.il/epg/tv10poc.xml"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "channels"
        ],
        "summary": "Set the scheduled EPG source of a channel (XMLTV URL or Redge portal JSON)",
        "description": "**Required scope:** `channels:write`\n\nReplaces the channel's EPG source settings; unset fields take the defaults (format xmltv, interval 60 min, 3 days\nahead, lang he). `source_url` (http/https) is required when `enabled`; it is fetched server-side, so private,\nloopback and other non-public targets are refused (422 `source_url`). The importer pulls every\n`interval_min` minutes and, with `daily_at`, once a day at that guide-timezone time. Does not import now — use\nPOST …/epg/import-now. Body ≤ 16 KB. Answers the same document as GET. Audited as `channel.epg.update`.",
        "operationId": "putChannelEPG",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EPGConfig"
              },
              "example": {
                "enabled": true,
                "format": "redge_json",
                "source_url": "https://vod.tv10.co.il/api/products/lives/programmes?platform=BROWSER&liveId%5B%5D=790191",
                "interval_min": 60,
                "days_ahead": 3,
                "lang": "he"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "EPG settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelEPG"
                },
                "example": {
                  "config": {
                    "enabled": true,
                    "source_url": "https://vod.tv10.co.il/api/products/lives/programmes?platform=BROWSER&liveId%5B%5D=790191",
                    "format": "redge_json",
                    "interval_min": 60,
                    "days_ahead": 3,
                    "lang": "he"
                  },
                  "status": {},
                  "export": {
                    "xmltv": "https://api.viewstream.co.il/epg/tv10poc/main.xml",
                    "json": "https://api.viewstream.co.il/epg/tv10poc/main.json",
                    "tenant_xmltv": "https://api.viewstream.co.il/epg/tv10poc.xml"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 16 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/channels/{id}/epg/import-now": {
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Fetch the configured EPG source now (synchronous, 30 s fetch timeout)",
        "description": "**Required scope:** `channels:write`\n\nRuns the channel's configured EPG import immediately (trigger `manual`) and answers its result; with EPG\npublishing in review / staged-import mode the changes go to the draft (`staged: true`). No body. Audited as\n`channel.epg.import_now` (also when the fetch fails).",
        "operationId": "importChannelEPGNow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Import result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGImportResult"
                },
                "example": {
                  "source": "manual",
                  "created": 2,
                  "updated": 58,
                  "deleted": 0,
                  "skipped": 0,
                  "errors": [],
                  "from": "2026-10-06T00:00:00+03:00",
                  "to": "2026-10-09T00:00:00+03:00"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "No EPG source configured for this channel (problem type `validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The source could not be fetched or read (detail names the error)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/epg/catalog": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Metadata vocabularies of the guide — genres and Israeli age ratings (he/en labels)",
        "description": "**Required scope:** `channels:read`\n\nThe fixed vocabularies that programme metadata (`meta.genres`, `meta.rating`) accepts, with English and Hebrew\nlabels for pickers (ratings carry the same short label, e.g. `16+`, in both). Static per release; no channel or tenant data. Scope `channels:read`; platform tenants only\n(a CDN-only tenant gets 403 `feature_disabled`).",
        "operationId": "getEPGCatalog",
        "responses": {
          "200": {
            "description": "Catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGCatalog"
                },
                "example": {
                  "genres": [
                    {
                      "id": "news",
                      "en": "News",
                      "he": "חדשות"
                    },
                    {
                      "id": "current_affairs",
                      "en": "Current affairs",
                      "he": "אקטואליה"
                    },
                    {
                      "id": "sport",
                      "en": "Sport",
                      "he": "ספורט"
                    }
                  ],
                  "ratings": [
                    {
                      "id": "all",
                      "en": "0+",
                      "he": "0+"
                    },
                    {
                      "id": "8",
                      "en": "8+",
                      "he": "8+"
                    },
                    {
                      "id": "16",
                      "en": "16+",
                      "he": "16+"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/epg/workflow": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Editorial workflow of the channel's guide (auto | review, lock window) and the draft state",
        "description": "**Required scope:** `channels:read`\n\nThe channel's guide workflow — a channel that never set one gets the defaults (`auto`, `lock_hours` 0,\n`auto_publish_imports` true) — plus the state and size of its draft and whether the caller may publish\n(`can_publish`: scope `epg:publish` or `channels:write`). Scope `channels:read`.",
        "operationId": "getEPGWorkflow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id (a channel of the caller's tenant; unknown or trashed = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Workflow",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGWorkflow"
                },
                "example": {
                  "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                  "mode": "review",
                  "lock_hours": 3,
                  "auto_publish_imports": true,
                  "updated_at": "2026-10-01T08:12:44Z",
                  "draft_state": "draft",
                  "draft_ops": 4,
                  "can_publish": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "epg"
        ],
        "summary": "Set the workflow — `review` stages edits in a draft for a publisher; `lock_hours` protects the next hours",
        "description": "**Required scope:** `epg:publish` or `channels:write`\n\nPartial update: omitted fields keep their value. `auto` (default) publishes every edit and import directly as a\nversion; `review` stages editors' changes in the channel's draft until a publisher publishes it\n(`auto_publish_imports` decides whether scheduled imports still publish directly). `lock_hours` (0–48) protects\nthe next hours of the guide: a change touching them needs a publisher and `?confirm_lock=1` (409 `lock_window`\notherwise). Scope `epg:publish` (or `channels:write`). Audited as `channel.epg.workflow`. Returns the workflow\nview like GET.",
        "operationId": "putEPGWorkflow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "review"
                    ]
                  },
                  "lock_hours": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 48
                  },
                  "auto_publish_imports": {
                    "type": "boolean",
                    "description": "review mode: scheduled imports still publish directly"
                  }
                }
              },
              "example": {
                "mode": "review",
                "lock_hours": 3,
                "auto_publish_imports": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The workflow after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGWorkflow"
                },
                "example": {
                  "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                  "mode": "review",
                  "lock_hours": 3,
                  "auto_publish_imports": true,
                  "updated_at": "2026-10-06T09:30:00Z",
                  "draft_state": "empty",
                  "draft_ops": 0,
                  "can_publish": true
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON or larger than 16 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:publish or channels:write"
      }
    },
    "/v1/channels/{id}/epg/draft": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "The draft over a window",
        "description": "**Required scope:** `channels:read`\n\nThe draft over a window — rows as they will be after publishing (flagged added/changed/removed), the diff, validation issues\n\nResolves the channel's draft (the staged operations) against the published guide of the window: `rows` are the\nprogrammes as they will be after publishing, each changed one flagged `added` / `changed` / `removed` (removed\nrows are included, flagged); `diff` and `summary` cover the whole draft, also outside the window; `issues` are\nthe validation results of the window as the draft would make it, plus contradictions the last import found in\nthe source (`source_conflict` warnings). `touches_lock` = publishing would change the lock window. A channel\nwithout a draft returns `state: empty` with no ops. Scope `channels:read`.",
        "operationId": "getEPGDraft",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Window start, RFC 3339 (default now − 24 h)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Window end, RFC 3339 (default now + 7 days); after `from`, window at most 22 days",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Draft",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGDraft"
                },
                "example": {
                  "workflow": {
                    "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                    "mode": "review",
                    "lock_hours": 3,
                    "auto_publish_imports": true,
                    "updated_at": "2026-10-01T08:12:44Z"
                  },
                  "state": "in_review",
                  "submitted_by": "019a3c10-1111-7a2b-8c3d-4e5f60718293",
                  "submitted_at": "2026-10-06T07:58:10Z",
                  "note": "ערב חג — שינוי לוח",
                  "ops": [
                    {
                      "id": "019a3c22-0a01-7c00-8c00-0000000000a1",
                      "kind": "update",
                      "programme_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                      "data": {
                        "title": "חדשות הערב — מהדורה מיוחדת"
                      },
                      "source": "editor",
                      "author": "019a3c10-1111-7a2b-8c3d-4e5f60718293",
                      "created_at": "2026-10-06T07:55:02Z"
                    }
                  ],
                  "rows": [
                    {
                      "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                      "start_at": "2026-10-06T17:00:00Z",
                      "end_at": "2026-10-06T18:00:00Z",
                      "title": "חדשות הערב — מהדורה מיוחדת",
                      "source": "epg",
                      "external_id": "redge:1861200",
                      "meta": {
                        "lang": "he",
                        "category": "חדשות",
                        "rating": "all"
                      },
                      "status": "changed"
                    },
                    {
                      "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d002",
                      "start_at": "2026-10-06T18:00:00Z",
                      "end_at": "2026-10-06T19:00:00Z",
                      "title": "הפטריוטים",
                      "source": "epg",
                      "external_id": "redge:1861201",
                      "meta": {
                        "lang": "he"
                      }
                    }
                  ],
                  "diff": {
                    "added": [],
                    "removed": [],
                    "changed": [
                      {
                        "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                        "fields": [
                          "title"
                        ],
                        "before": {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start_at": "2026-10-06T17:00:00Z",
                          "end_at": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב",
                          "source": "epg"
                        },
                        "after": {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start_at": "2026-10-06T17:00:00Z",
                          "end_at": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב — מהדורה מיוחדת",
                          "source": "epg"
                        }
                      }
                    ]
                  },
                  "summary": {
                    "added": 0,
                    "changed": 1,
                    "removed": 0,
                    "from": "2026-10-06T17:00:00Z",
                    "to": "2026-10-06T18:00:00Z"
                  },
                  "issues": [
                    {
                      "level": "warning",
                      "code": "missing_image",
                      "programme_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d002",
                      "at": "2026-10-06T18:00:00Z",
                      "message": "\"הפטריוטים\" has no image",
                      "params": {
                        "title": "הפטריוטים",
                        "field": "image"
                      }
                    }
                  ],
                  "touches_lock": false,
                  "can_publish": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid window (`from`/`to` not RFC 3339, `to` not after `from`, or more than 22 days)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/epg/draft/ops": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Stage operations (create / update / delete a programme) in the draft",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nAdds 1–2000 operations to the channel's draft whatever the workflow mode (nothing is published; a publisher\npublishes the draft). `create` needs `data.start_at` and a non-empty `data.title` and gets a new programme id;\n`update` needs `data` (only the given fields change); `update` / `delete` must target a programme of this\nchannel or one created earlier in the draft. `data.meta` is checked against the metadata vocabulary\n(see GET /v1/epg/catalog), `end_at` must be after `start_at`, titles are 1–300 characters, `external_id` is kept\non `create` only. A draft holds at most 5000 operations (422). Editing a draft that is `in_review` moves it back\nto `draft`. Scope `epg:write` (or `channels:write`). Audited as `channel.epg.draft.ops`. Returns the draft over\nthe default window (like GET …/epg/draft without parameters).",
        "operationId": "postEPGDraftOps",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ops"
                ],
                "properties": {
                  "ops": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 2000,
                    "items": {
                      "$ref": "#/components/schemas/EPGOpInput"
                    }
                  }
                }
              },
              "example": {
                "ops": [
                  {
                    "kind": "update",
                    "programme_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                    "data": {
                      "title": "חדשות הערב",
                      "meta": {
                        "rating": "all",
                        "rights": {
                          "catchup": false
                        }
                      }
                    }
                  },
                  {
                    "kind": "create",
                    "data": {
                      "start_at": "2026-10-07T21:00:00Z",
                      "end_at": "2026-10-07T22:00:00Z",
                      "title": "פגוש את העיתונות",
                      "meta": {
                        "genres": [
                          "current_affairs"
                        ]
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The draft after staging (default window now − 24 h … now + 7 days); same shape as GET …/epg/draft",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGDraft"
                },
                "example": {
                  "workflow": {
                    "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                    "mode": "review",
                    "lock_hours": 3,
                    "auto_publish_imports": true,
                    "updated_at": "2026-10-01T08:12:44Z"
                  },
                  "state": "draft",
                  "ops": [
                    {
                      "id": "019a3c22-0a01-7c00-8c00-0000000000a2",
                      "kind": "create",
                      "programme_id": "019a3c22-0a01-7c00-8c00-0000000000b2",
                      "data": {
                        "start_at": "2026-10-07T21:00:00Z",
                        "end_at": "2026-10-07T22:00:00Z",
                        "title": "פגוש את העיתונות",
                        "meta": {
                          "genres": [
                            "current_affairs"
                          ]
                        }
                      },
                      "source": "editor",
                      "author": "019a3c10-1111-7a2b-8c3d-4e5f60718293",
                      "created_at": "2026-10-06T09:41:00Z"
                    }
                  ],
                  "rows": [
                    {
                      "id": "019a3c22-0a01-7c00-8c00-0000000000b2",
                      "start_at": "2026-10-07T21:00:00Z",
                      "end_at": "2026-10-07T22:00:00Z",
                      "title": "פגוש את העיתונות",
                      "source": "manual",
                      "meta": {
                        "genres": [
                          "current_affairs"
                        ]
                      },
                      "status": "added"
                    }
                  ],
                  "diff": {
                    "added": [
                      {
                        "id": "019a3c22-0a01-7c00-8c00-0000000000b2",
                        "start_at": "2026-10-07T21:00:00Z",
                        "end_at": "2026-10-07T22:00:00Z",
                        "title": "פגוש את העיתונות",
                        "source": "manual",
                        "meta": {
                          "genres": [
                            "current_affairs"
                          ]
                        }
                      }
                    ],
                    "removed": [],
                    "changed": []
                  },
                  "summary": {
                    "added": 1,
                    "changed": 0,
                    "removed": 0,
                    "from": "2026-10-07T21:00:00Z",
                    "to": "2026-10-07T22:00:00Z"
                  },
                  "issues": [],
                  "touches_lock": false,
                  "can_publish": false
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 4 MiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid operations — `ops` empty or over 2000, an unknown `programme_id` (`errors[].field` = `ops[i].programme_id`), invalid `meta`, `end_at` not after `start_at`, missing title / start, or the draft would exceed 5000 operations",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/draft/ops/{oid}": {
      "delete": {
        "tags": [
          "epg"
        ],
        "summary": "Drop one staged operation",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nRemoves one operation from the channel's draft (ids are in `ops[].id` of GET …/epg/draft). Other operations stay and\nthe draft keeps its state. Scope `epg:write` (or `channels:write`). Not audited.",
        "operationId": "deleteEPGDraftOp",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "oid",
            "in": "path",
            "required": true,
            "description": "Operation id from the draft's `ops[].id`",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel, or no such operation in its draft",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/draft/discard": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Discard the whole draft",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nDrops every staged operation of the channel's draft (state back to `empty`); the published guide is untouched.\nDiscarding an empty draft also answers 204. Scope `epg:write` (or `channels:write`). Audited as\n`channel.epg.draft.discard`.",
        "operationId": "discardEPGDraft",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Discarded (or there was nothing to discard)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/draft/submit": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Submit the draft for review (state in_review; a new edit moves it back to draft)",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nMarks the channel's draft `in_review` with the caller as submitter and an optional note, so a publisher can\nreview and publish it. Any later staged edit moves it back to `draft`. An unparsable body is ignored (no note).\nScope `epg:write` (or `channels:write`). Audited as `channel.epg.draft.submit`. Returns the draft over the\ndefault window.",
        "operationId": "submitEPGDraft",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "for the publisher"
                  }
                }
              },
              "example": {
                "note": "ערב חג — שינוי לוח"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The draft, now `in_review` (same shape as GET …/epg/draft)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGDraft"
                },
                "example": {
                  "workflow": {
                    "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                    "mode": "review",
                    "lock_hours": 3,
                    "auto_publish_imports": true,
                    "updated_at": "2026-10-01T08:12:44Z"
                  },
                  "state": "in_review",
                  "submitted_by": "019a3c10-1111-7a2b-8c3d-4e5f60718293",
                  "submitted_at": "2026-10-06T07:58:10Z",
                  "note": "ערב חג — שינוי לוח",
                  "ops": [
                    {
                      "id": "019a3c22-0a01-7c00-8c00-0000000000a1",
                      "kind": "update",
                      "programme_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                      "data": {
                        "title": "חדשות הערב — מהדורה מיוחדת"
                      },
                      "source": "editor",
                      "author": "019a3c10-1111-7a2b-8c3d-4e5f60718293",
                      "created_at": "2026-10-06T07:55:02Z"
                    }
                  ],
                  "rows": [],
                  "diff": {
                    "added": [],
                    "removed": [],
                    "changed": []
                  },
                  "summary": {
                    "added": 0,
                    "changed": 1,
                    "removed": 0
                  },
                  "issues": [],
                  "touches_lock": false,
                  "can_publish": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The draft is empty (`conflict`, \"nothing to publish\")",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`note` longer than 1000 characters",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/draft/publish": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Publish the draft as one version (webhook `epg.published`; on-publish destinations are delivered)",
        "description": "**Required scope:** `epg:publish` or `channels:write`\n\nApplies the whole draft as one new version and clears the draft. `ops` = the number of operations the\npublisher reviewed: if the draft has a different number now, 409 (reload and review again). A draft that touches\nthe lock window needs `?confirm_lock=1` (409 `lock_window` otherwise). Overlaps and missing titles that involve\nthe draft's programmes block publishing (422 with `issues`) unless `force` is true; warnings never block.\nImport deletions of programmes that already aired are skipped. A draft that changes nothing is cleared and\nanswers 409. On success the webhook `epg.published` fires and the tenant's `on_publish` destinations that\ninclude the channel are marked pending (delivered after a minute of quiet). Scope `epg:publish` (or\n`channels:write`). Audited as `channel.epg.publish`.",
        "operationId": "publishEPGDraft",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "`1` or `true` confirms a change inside the lock window",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "stored on the version"
                  },
                  "ops": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "operations the publisher reviewed (optional optimistic check)"
                  },
                  "force": {
                    "type": "boolean",
                    "default": false,
                    "description": "publish despite blocking errors (overlaps, missing titles)"
                  }
                }
              },
              "example": {
                "note": "ערב חג — שינוי לוח",
                "ops": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new version (with its diff)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGVersion"
                },
                "example": {
                  "id": "019a3c30-5b00-7d11-9e22-000000000031",
                  "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                  "n": 31,
                  "source": "publish",
                  "author": "019a3c10-2222-7a2b-8c3d-4e5f60718294",
                  "note": "ערב חג — שינוי לוח",
                  "summary": {
                    "added": 1,
                    "changed": 1,
                    "removed": 0,
                    "from": "2026-10-06T17:00:00Z",
                    "to": "2026-10-07T22:00:00Z"
                  },
                  "diff": {
                    "added": [
                      {
                        "id": "019a3c22-0a01-7c00-8c00-0000000000b2",
                        "start_at": "2026-10-07T21:00:00Z",
                        "end_at": "2026-10-07T22:00:00Z",
                        "title": "פגוש את העיתונות",
                        "source": "manual",
                        "meta": {
                          "genres": [
                            "current_affairs"
                          ]
                        }
                      }
                    ],
                    "removed": [],
                    "changed": [
                      {
                        "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                        "fields": [
                          "title"
                        ],
                        "before": {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start_at": "2026-10-06T17:00:00Z",
                          "end_at": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב",
                          "source": "epg"
                        },
                        "after": {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start_at": "2026-10-06T17:00:00Z",
                          "end_at": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב — מהדורה מיוחדת",
                          "source": "epg"
                        }
                      }
                    ]
                  },
                  "created_at": "2026-10-06T08:20:31Z"
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 16 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`lock_window` (repeat with `?confirm_lock=1`), or `conflict`: the draft changed since it was reviewed, or there is nothing to publish",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`note` too long, or the draft has blocking errors: the problem carries `issues` (the error-level issues of the programmes it touches); fix them or publish with `force`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Problem"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "issues": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPGIssue"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "code": "validation_error",
                  "detail": "the draft has errors (overlaps or missing titles); fix them or publish with force",
                  "issues": [
                    {
                      "level": "error",
                      "code": "overlap",
                      "programme_id": "019a3c22-0a01-7c00-8c00-0000000000b2",
                      "at": "2026-10-07T21:00:00Z",
                      "message": "\"פגוש את העיתונות\" overlaps \"סרט הערב\" by 15m0s",
                      "params": {
                        "title": "פגוש את העיתונות",
                        "other_title": "סרט הערב",
                        "seconds": 900,
                        "field": "start"
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:publish or channels:write"
      }
    },
    "/v1/channels/{id}/epg/versions": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Published versions of the channel's guide, newest first (without the diff body)",
        "description": "**Required scope:** `channels:read`\n\nEvery change that reached the published guide is a version: an edit or bulk tool in auto mode, a published\ndraft, an import, a rollback (`source`). Items carry `summary` but not the diff (`diff` arrays are null here;\nGET …/versions/{n} has it). Page with `before` = the smallest `n` of the previous page. Versions older than\n90 days are pruned. Scope `channels:read`.",
        "operationId": "listEPGVersions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size; values outside 1–200 fall back to 50",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "before",
            "in": "query",
            "description": "Only versions with n < before (paging); a non-integer is ignored",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPGVersion"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "019a3c30-5b00-7d11-9e22-000000000031",
                      "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                      "n": 31,
                      "source": "publish",
                      "author": "019a3c10-2222-7a2b-8c3d-4e5f60718294",
                      "note": "ערב חג — שינוי לוח",
                      "summary": {
                        "added": 1,
                        "changed": 1,
                        "removed": 0,
                        "from": "2026-10-06T17:00:00Z",
                        "to": "2026-10-07T22:00:00Z"
                      },
                      "diff": {
                        "added": null,
                        "removed": null,
                        "changed": null
                      },
                      "created_at": "2026-10-06T08:20:31Z"
                    },
                    {
                      "id": "019a3b90-11aa-7c3d-8e4f-000000000030",
                      "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                      "n": 30,
                      "source": "import",
                      "summary": {
                        "added": 0,
                        "changed": 9,
                        "removed": 0,
                        "from": "2026-10-05T03:00:10Z",
                        "to": "2026-10-05T19:30:17Z"
                      },
                      "diff": {
                        "added": null,
                        "removed": null,
                        "changed": null
                      },
                      "created_at": "2026-10-05T02:45:03Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/epg/versions/{n}": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "One version with its diff",
        "description": "**Required scope:** `channels:read`\n\nOne published version of the channel's guide with the full diff (rows before and after, changed fields).\nScope `channels:read`.",
        "operationId": "getEPGVersion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "n",
            "in": "path",
            "required": true,
            "description": "Version number (1, 2, …); anything else = 404",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGVersion"
                },
                "example": {
                  "id": "019a3c30-5b00-7d11-9e22-000000000031",
                  "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                  "n": 31,
                  "source": "publish",
                  "author": "019a3c10-2222-7a2b-8c3d-4e5f60718294",
                  "note": "ערב חג — שינוי לוח",
                  "summary": {
                    "added": 1,
                    "changed": 1,
                    "removed": 0,
                    "from": "2026-10-06T17:00:00Z",
                    "to": "2026-10-07T22:00:00Z"
                  },
                  "diff": {
                    "added": [
                      {
                        "id": "019a3c22-0a01-7c00-8c00-0000000000b2",
                        "start_at": "2026-10-07T21:00:00Z",
                        "end_at": "2026-10-07T22:00:00Z",
                        "title": "פגוש את העיתונות",
                        "source": "manual",
                        "meta": {
                          "genres": [
                            "current_affairs"
                          ]
                        }
                      }
                    ],
                    "removed": [],
                    "changed": [
                      {
                        "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                        "fields": [
                          "title"
                        ],
                        "before": {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start_at": "2026-10-06T17:00:00Z",
                          "end_at": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב",
                          "source": "epg"
                        },
                        "after": {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start_at": "2026-10-06T17:00:00Z",
                          "end_at": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב — מהדורה מיוחדת",
                          "source": "epg"
                        }
                      }
                    ]
                  },
                  "created_at": "2026-10-06T08:20:31Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel or version",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/epg/versions/{n}/rollback": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Undo version n as a new version (`dry_run` returns the diff only)",
        "description": "**Required scope:** `epg:publish` or `channels:write`\n\nUndoes version `n` against the current guide: programmes it added are removed, programmes it changed or removed\nget their previous state back. Always published directly as a new version (`source: rollback`,\n`rollback_of: n`), whatever the workflow mode — it is a publisher's action. `dry_run` returns\n`{diff, summary, touches_lock}` without publishing. A rollback that touches the lock window needs\n`?confirm_lock=1`. The body is optional (empty = no options); a malformed body or an unknown field answers 400.\nDefault note: \"rollback of version n\". Fires `epg.published` like any version. Scope `epg:publish` (or\n`channels:write`). Audited as `channel.epg.rollback`.",
        "operationId": "rollbackEPGVersion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "n",
            "in": "path",
            "required": true,
            "description": "The version to undo",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "`1` or `true` confirms a change inside the lock window",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "dry_run": {
                    "type": "boolean",
                    "default": false
                  },
                  "note": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "dry_run": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new version, or for a dry run the plan `{diff, summary, touches_lock}`",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/EPGVersion"
                    },
                    {
                      "$ref": "#/components/schemas/EPGRollbackPreview"
                    }
                  ]
                },
                "example": {
                  "diff": {
                    "added": [],
                    "removed": [],
                    "changed": [
                      {
                        "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                        "fields": [
                          "title"
                        ],
                        "before": {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start_at": "2026-10-06T17:00:00Z",
                          "end_at": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב — מהדורה מיוחדת",
                          "source": "epg"
                        },
                        "after": {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start_at": "2026-10-06T17:00:00Z",
                          "end_at": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב",
                          "source": "epg"
                        }
                      }
                    ]
                  },
                  "summary": {
                    "added": 0,
                    "changed": 1,
                    "removed": 0,
                    "from": "2026-10-06T17:00:00Z",
                    "to": "2026-10-06T18:00:00Z"
                  },
                  "touches_lock": false
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body, unknown field or body over 16 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel or version",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`lock_window` (repeat with `?confirm_lock=1`), or `conflict`: the rollback changes nothing (\"nothing to publish\")",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:publish or channels:write"
      }
    },
    "/v1/channels/{id}/epg/validate": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Gaps, overlaps, short programmes, missing titles / images / prime-time ratings in a window…",
        "description": "**Required scope:** `channels:read`\n\nGaps, overlaps, short programmes, missing titles / images / prime-time ratings in a window (`draft=1` = as the draft would make it)\n\nChecks the published guide of a window (default now … now + 7 days) — or, with `draft=1`, the window as the\ndraft would make it — and adds contradictions the channel's last import found in its source\n(`source_conflict` warnings). `error` issues (overlaps over 30 s, missing titles) block publishing a draft;\nwarnings do not. Scope `channels:read`.",
        "operationId": "validateEPG",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Window start, RFC 3339 (default now)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Window end, RFC 3339 (default now + 7 days); window at most 22 days",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "draft",
            "in": "query",
            "description": "`1` or `true`: validate the window as the draft would make it",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Issues (empty array = clean)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "issues"
                  ],
                  "properties": {
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPGIssue"
                      }
                    }
                  }
                },
                "example": {
                  "issues": [
                    {
                      "level": "warning",
                      "code": "gap",
                      "programme_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d002",
                      "at": "2026-10-06T19:00:00Z",
                      "message": "10m0s without a programme after \"הפטריוטים\"",
                      "params": {
                        "title": "הפטריוטים",
                        "seconds": 600,
                        "field": "end"
                      }
                    },
                    {
                      "level": "warning",
                      "code": "missing_rating",
                      "programme_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d003",
                      "at": "2026-10-06T19:10:00Z",
                      "message": "\"סרט הערב\" airs in prime time without an age rating",
                      "params": {
                        "title": "סרט הערב",
                        "field": "rating"
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid window (not RFC 3339, `to` not after `from`, or more than 22 days)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/epg/fix": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "What a one-click fix of a publishing warning would do",
        "description": "**Required scope:** `channels:read`\n\nWhat a one-click fix of a publishing warning would do — the recording still for `missing_image`, and how many programmes and recurring slots of the series still lack the field\n\nRead-only preview for POST …/epg/fix. `series` counts the programmes of the same series (title match) from a day\nback to 14 days ahead that still lack the field, and the recurring slots (templates) of that title without it.\nFor `field=image`, `image` is the still a fix would use: the programme's (or its series' latest aired\nprogramme's) smart poster, else a frame of the recording within the channel's retention (≤ 7 days); null when\nnone is available (and always null for a CDN-only tenant). `default` is the series default already stored;\n`can_default` = series defaults are available (migration 0035). Scope `channels:read`.",
        "operationId": "previewEPGFix",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "programme_id",
            "in": "query",
            "required": true,
            "description": "A programme of this channel (published)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "field",
            "in": "query",
            "required": true,
            "description": "What is missing",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "rating"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Preview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGFixPreview"
                },
                "example": {
                  "programme_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d002",
                  "field": "image",
                  "image": {
                    "path": "/rec/tv10poc/main/thumbs/20261005/1710/1791220200.jpg",
                    "url": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/thumbs/20261005/1710/1791220200.jpg",
                    "kind": "still",
                    "source_programme_id": "019a3b10-7d2e-7f41-9b0c-5e2a41c0d0f1",
                    "source_start_at": "2026-10-05T17:00:00Z"
                  },
                  "series": {
                    "title": "הפטריוטים",
                    "programmes": 6,
                    "templates": 1,
                    "from": "2026-10-05T09:30:00Z",
                    "to": "2026-10-20T09:30:00Z"
                  },
                  "can_default": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel, or the programme is not on this channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`field` not image|rating, or `programme_id` not a uuid",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Fix a `missing_image` / `missing_rating` warning for one programme or its whole series…",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nFix a `missing_image` / `missing_rating` warning for one programme or its whole series (a guide change through the workflow; a series fix also stores a series default the importer applies and fills the series' recurring slots)\n\nSets `meta.image` or `meta.rating` (other meta keys are kept) on one programme (`scope: programme`, an\neditor's change) or on every programme of its series from a day back to 14 days ahead that still lacks it\n(`scope: series`; imported programmes stay attached to the feed). The change goes through the channel's workflow:\nstaged in the draft in review mode or with `stage`, else published as a version (lock window: 409 unless a\npublisher sends `?confirm_lock=1`). For `field=image` without `image_path` the recording still is used (see\nGET …/epg/fix); a still under `/rec/` is first copied to the tenant's VOD storage so it outlives the recording.\nA series fix also stores a series default (when migration 0035 is applied; `default_saved`) and fills the\nseries' recurring slots (`templates`). Scope `epg:write` (or `channels:write`). Audited as `channel.epg.fix`.",
        "operationId": "fixEPG",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "`1` or `true` confirms a change inside the lock window (publishers only)",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "programme_id",
                  "field"
                ],
                "properties": {
                  "programme_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "field": {
                    "type": "string",
                    "enum": [
                      "image",
                      "rating"
                    ]
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "programme",
                      "series"
                    ],
                    "default": "programme"
                  },
                  "rating": {
                    "type": "string",
                    "enum": [
                      "all",
                      "8",
                      "12",
                      "14",
                      "16",
                      "18"
                    ],
                    "description": "required for field=rating"
                  },
                  "image_path": {
                    "type": "string",
                    "description": "field=image: a JPEG/PNG path under /rec/<tenant>/ or /vod/<tenant>/; empty = the recording still (smart poster first). Recording stills are copied to VOD storage."
                  },
                  "stage": {
                    "type": "boolean",
                    "default": false,
                    "description": "put it in the draft even in auto mode"
                  }
                }
              },
              "example": {
                "programme_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d003",
                "field": "rating",
                "scope": "series",
                "rating": "14"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result: the commit (`staged` or the new `version`), how many programmes and slots were filled, whether the series default was saved, and the image used",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGFixResult"
                },
                "example": {
                  "staged": true,
                  "ops": 4,
                  "programmes": 4,
                  "templates": 1,
                  "default_saved": true
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 16 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel, or the programme is not on this channel",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`lock_window`: the change touches the lock window (a publisher repeats with `?confirm_lock=1`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid `field` / `scope` / `rating` / `programme_id` / `image_path`, no recording still available for the programme or its series (choose `image_path`), or the still left the recording meanwhile",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The still could not be read from or stored to object storage",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027), or storage is not available to copy the still (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/series-defaults": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "The channel's per-series image / age rating that the importer fills into programmes the feed leaves without…",
        "description": "**Required scope:** `channels:read`\n\nThe channel's per-series image / age rating that the importer fills into programmes the feed leaves without one\n\nSeries defaults are stored by a series fix (POST …/epg/fix with `scope: series`). On every later import the\nimporter fills them into programmes of that series (matched by a normalised title, `series_key`) that arrive\nwithout an image or rating. Scope `channels:read`.",
        "operationId": "listEPGSeriesDefaults",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Series defaults",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPGSeriesDefault"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                      "series_key": "הפטריוטים",
                      "title": "הפטריוטים",
                      "image": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/posters/programme/019a3b10-7d2e-7f41-9b0c-5e2a41c0d0f1/3fa2c1d09b7e4a55.jpg",
                      "rating": "14",
                      "updated_by": "key:vs_t10Kx7Qp",
                      "updated_at": "2026-10-05T12:40:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "delete": {
        "tags": [
          "epg"
        ],
        "summary": "Forget a series default (`field` empty = image and rating); programmes already filled keep their values",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nRemoves the stored image and/or rating default of one series (matched like the importer does, by normalised\ntitle) so later imports stop filling it. Programmes and recurring slots already filled are not changed.\nScope `epg:write` (or `channels:write`). Audited as `channel.epg.series_default.delete`.",
        "operationId": "deleteEPGSeriesDefault",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "title",
            "in": "query",
            "required": true,
            "description": "The series title (as in `title` of the default)",
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "field",
            "in": "query",
            "description": "Forget only this field; empty = both",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "rating"
              ]
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel (or no such series default)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`title` missing or `field` not image|rating",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/copy": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Copy a period (a day or a week, ≤ 8 days) so it starts at `target`",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nCopy a period (a day or a week, ≤ 8 days) so it starts at `target`; `replace` removes what starts in the target period first\n\nCopies the published programmes of [`from`, `to`) (at most 8 days; `target` ≠ `from`) so that the period starts\nat `target`, as new manual programmes. With `replace`, programmes that start inside the target period are\ndeleted first. The change goes through the channel's workflow: staged in the draft in review mode or with `stage`\n(`staged: true`), else published at once as one version (`version`; webhook `epg.published`, on-publish\ndestinations marked pending). In auto mode a change touching the lock window needs a publisher and `?confirm_lock=1`.\nScope `epg:write` (or `channels:write`). Audited as `channel.epg.copy`.",
        "operationId": "copyEPG",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "`1` or `true` confirms a change inside the lock window (publishers only)",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "from",
                  "to",
                  "target"
                ],
                "properties": {
                  "from": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "to": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "target": {
                    "type": "string",
                    "format": "date-time",
                    "description": "where the copied period starts"
                  },
                  "replace": {
                    "type": "boolean",
                    "default": false,
                    "description": "delete programmes that start in the target period first"
                  },
                  "stage": {
                    "type": "boolean",
                    "default": false,
                    "description": "put it in the draft even in auto mode"
                  }
                }
              },
              "example": {
                "from": "2026-10-05T03:00:00Z",
                "to": "2026-10-06T03:00:00Z",
                "target": "2026-10-12T03:00:00Z",
                "replace": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result: `staged` (review mode / `stage`) or the published `version`; `ops` = operations generated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGCommitResult"
                },
                "example": {
                  "staged": false,
                  "version": {
                    "id": "019a3c41-0c00-7a00-8b00-000000000032",
                    "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                    "n": 32,
                    "source": "copy",
                    "author": "019a3c10-2222-7a2b-8c3d-4e5f60718294",
                    "summary": {
                      "added": 18,
                      "changed": 0,
                      "removed": 2,
                      "from": "2026-10-12T03:00:00Z",
                      "to": "2026-10-13T03:00:00Z"
                    },
                    "diff": {
                      "added": [],
                      "removed": [],
                      "changed": []
                    },
                    "created_at": "2026-10-06T10:02:11Z"
                  },
                  "ops": 20
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 16 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`lock_window`: the change touches the lock window (a publisher repeats with `?confirm_lock=1`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`from` / `to` / `target` missing, `to` not after `from`, more than 8 days, target equals the source, nothing to copy, or the draft would exceed 5000 operations",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/shift": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Move every programme starting in a period (≤ 48 h) by ± minutes (an overrun)",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nMoves every published programme that starts in [`from`, `to`) (at most 48 h) by `minutes` (−720…720, not 0),\ne.g. after a live overrun. The change goes through the channel's workflow: staged in the draft in review mode or with `stage`\n(`staged: true`), else published at once as one version (`version`; webhook `epg.published`, on-publish\ndestinations marked pending). In auto mode a change touching the lock window needs a publisher and `?confirm_lock=1`.\nScope `epg:write` (or `channels:write`). Audited as `channel.epg.shift`.",
        "operationId": "shiftEPG",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "`1` or `true` confirms a change inside the lock window (publishers only)",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "from",
                  "to",
                  "minutes"
                ],
                "properties": {
                  "from": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "to": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "minutes": {
                    "type": "integer",
                    "minimum": -720,
                    "maximum": 720,
                    "description": "not 0"
                  },
                  "stage": {
                    "type": "boolean",
                    "default": false,
                    "description": "put it in the draft even in auto mode"
                  }
                }
              },
              "example": {
                "from": "2026-10-06T19:00:00Z",
                "to": "2026-10-06T23:59:00Z",
                "minutes": 15
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result: `staged` or the published `version`; `ops` = programmes moved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGCommitResult"
                },
                "example": {
                  "staged": true,
                  "ops": 5
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 16 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`lock_window`: the change touches the lock window (a publisher repeats with `?confirm_lock=1`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`minutes` 0 or outside −720…720, `to` not after `from`, period over 48 h, no programme in the period (\"nothing to change\"), or the draft would exceed 5000 operations",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/templates": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Recurring slots of the channel (\"Stock Talk Sun–Thu 09:00, 55 min\")",
        "description": "**Required scope:** `channels:read`\n\nThe channel's recurring slots (templates). They create nothing by themselves: POST …/epg/templates/apply fills a\nperiod from them. Times are in the guide time zone (Asia/Jerusalem). Scope `channels:read`.",
        "operationId": "listEPGTemplates",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPGTemplate"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "019a3c50-3a00-7b00-8c00-0000000000f1",
                      "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                      "title": "שיחת מניות",
                      "weekdays": [
                        0,
                        1,
                        2,
                        3,
                        4
                      ],
                      "start_time": "09:00",
                      "duration_min": 55,
                      "meta": {
                        "genres": [
                          "business"
                        ],
                        "rating": "all"
                      },
                      "enabled": true,
                      "created_at": "2026-10-01T07:00:00Z",
                      "updated_at": "2026-10-01T07:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Create a recurring slot",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nAdds a recurring slot to the channel: a title on some weekdays (0 = Sunday … 6 = Saturday) at `start_time`\n(HH:MM, guide time zone) for `duration_min`, with optional programme `meta` (checked against the metadata\nvocabulary). `enabled` defaults to false when omitted. The guide is not changed until the slots are applied.\nScope `epg:write` (or `channels:write`). Audited as `channel.epg.template.put`.",
        "operationId": "createEPGTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EPGTemplate"
              },
              "example": {
                "title": "שיחת מניות",
                "weekdays": [
                  0,
                  1,
                  2,
                  3,
                  4
                ],
                "start_time": "09:00",
                "duration_min": 55,
                "meta": {
                  "genres": [
                    "business"
                  ],
                  "rating": "all"
                },
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGTemplate"
                },
                "example": {
                  "id": "019a3c50-3a00-7b00-8c00-0000000000f1",
                  "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                  "title": "שיחת מניות",
                  "weekdays": [
                    0,
                    1,
                    2,
                    3,
                    4
                  ],
                  "start_time": "09:00",
                  "duration_min": 55,
                  "meta": {
                    "genres": [
                      "business"
                    ],
                    "rating": "all"
                  },
                  "enabled": true,
                  "created_at": "2026-10-01T07:00:00Z",
                  "updated_at": "2026-10-01T07:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 64 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid template: `title` 1–300 chars, `weekdays` at least one of 0–6, `start_time` HH:MM, `duration_min` 1–1440, or invalid `meta` (`errors[]` lists the fields)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/templates/{tid}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "tid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "put": {
        "tags": [
          "epg"
        ],
        "summary": "Replace a recurring slot",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nReplaces every field of one recurring slot (omitted fields become empty / false — send the whole template).\nSame rules as create. Programmes already created from it are not changed. Scope `epg:write` (or\n`channels:write`). Audited as `channel.epg.template.put`.",
        "operationId": "putEPGTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "description": "Template id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EPGTemplate"
              },
              "example": {
                "title": "שיחת מניות",
                "weekdays": [
                  0,
                  1,
                  2,
                  3,
                  4
                ],
                "start_time": "09:05",
                "duration_min": 50,
                "meta": {
                  "genres": [
                    "business"
                  ],
                  "rating": "all"
                },
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGTemplate"
                },
                "example": {
                  "id": "019a3c50-3a00-7b00-8c00-0000000000f1",
                  "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                  "title": "שיחת מניות",
                  "weekdays": [
                    0,
                    1,
                    2,
                    3,
                    4
                  ],
                  "start_time": "09:05",
                  "duration_min": 50,
                  "meta": {
                    "genres": [
                      "business"
                    ],
                    "rating": "all"
                  },
                  "enabled": true,
                  "created_at": "2026-10-01T07:00:00Z",
                  "updated_at": "2026-10-06T10:15:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 64 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel or template",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid template: `title` 1–300 chars, `weekdays` at least one of 0–6, `start_time` HH:MM, `duration_min` 1–1440, or invalid `meta` (`errors[]` lists the fields)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      },
      "delete": {
        "tags": [
          "epg"
        ],
        "summary": "Delete a recurring slot (programmes already created stay)",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nDeletes one recurring slot; programmes already created from it stay in the guide. Scope `epg:write` (or\n`channels:write`). Audited as `channel.epg.template.delete`.",
        "operationId": "deleteEPGTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "description": "Template id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel or template",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/channels/{id}/epg/templates/apply": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Fill a period (≤ 22 days) from the enabled templates (all or `template_ids`)",
        "description": "**Required scope:** `epg:write` or `channels:write`\n\nFill a period (≤ 22 days) from the enabled templates (all or `template_ids`); an existing same-title programme within a minute is kept\n\nCreates a programme for every occurrence of the channel's enabled recurring slots (all, or only\n`template_ids`) in [`from`, `to`) (at most 22 days). An occurrence is skipped when a published programme with\nthe same title starts within a minute of it, so applying twice is harmless. When nothing is left to create the\nanswer is 200 with `ops: 0` and nothing changes. Otherwise the change goes through the workflow: staged in\nreview mode or with `stage`, else published as one version (`source: template`; lock window needs a publisher\nand `?confirm_lock=1`). Scope `epg:write` (or `channels:write`). Audited as `channel.epg.template.apply`.",
        "operationId": "applyEPGTemplates",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm_lock",
            "in": "query",
            "description": "`1` or `true` confirms a change inside the lock window (publishers only)",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "from",
                  "to"
                ],
                "properties": {
                  "from": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "to": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "template_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "only these templates (default all enabled)"
                  },
                  "stage": {
                    "type": "boolean",
                    "default": false,
                    "description": "put it in the draft even in auto mode"
                  }
                }
              },
              "example": {
                "from": "2026-10-11T00:00:00Z",
                "to": "2026-10-18T00:00:00Z",
                "template_ids": [
                  "019a3c50-3a00-7b00-8c00-0000000000f1"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result: `staged` or the published `version`; `ops` = programmes created (0 = nothing to do)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGCommitResult"
                },
                "example": {
                  "staged": false,
                  "version": {
                    "id": "019a3c41-0c00-7a00-8b00-000000000033",
                    "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                    "n": 33,
                    "source": "template",
                    "author": "019a3c10-2222-7a2b-8c3d-4e5f60718294",
                    "summary": {
                      "added": 5,
                      "changed": 0,
                      "removed": 0,
                      "from": "2026-10-11T06:00:00Z",
                      "to": "2026-10-15T06:55:00Z"
                    },
                    "diff": {
                      "added": [],
                      "removed": [],
                      "changed": []
                    },
                    "created_at": "2026-10-06T10:20:00Z"
                  },
                  "ops": 5
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 64 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`lock_window`: the change touches the lock window (a publisher repeats with `?confirm_lock=1`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`to` not after `from` or period over 22 days, or the draft would exceed 5000 operations",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:write or channels:write"
      }
    },
    "/v1/epg/destinations": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Publishing destinations of the tenant (partner pull feeds, HTTPS / SFTP / S3 pushes)",
        "description": "**Required scope:** `channels:read`\n\nEvery destination of the tenant, ordered by name, with its delivery state. Credentials and pull tokens are never\nreturned (`has_credentials`, `token_hint`; a pull feed's `feed_url` shows `{token}`). Scope `channels:read`.",
        "operationId": "listEPGDestinations",
        "responses": {
          "200": {
            "description": "Destinations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPGDestination"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "019a3c60-4b00-7c00-8d00-0000000000e1",
                      "customer_id": "019a3c00-0000-7000-8000-0000000000aa",
                      "name": "Partner pull feed",
                      "kind": "pull",
                      "format": "xmltv",
                      "channels": [
                        {
                          "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                          "external_id": "tv10.partner.example"
                        }
                      ],
                      "back_days": 1,
                      "ahead_days": 7,
                      "schedule": "manual",
                      "every_min": 60,
                      "target": {},
                      "has_credentials": false,
                      "token_hint": "epg_Q2x9vK…",
                      "feed_url": "https://api.viewstream.co.il/epg/d/{token}.xml",
                      "enabled": true,
                      "last_run_at": "2026-10-06T09:58:00Z",
                      "last_ok_at": "2026-10-06T09:58:00Z",
                      "last_status": "ok",
                      "created_at": "2026-10-01T07:30:00Z",
                      "updated_at": "2026-10-01T07:30:00Z"
                    },
                    {
                      "id": "019a3c60-4b00-7c00-8d00-0000000000e2",
                      "customer_id": "019a3c00-0000-7000-8000-0000000000aa",
                      "name": "Partner Yes",
                      "kind": "sftp",
                      "format": "xmltv",
                      "channels": [
                        {
                          "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                          "external_id": "tv10.yes.co.il"
                        }
                      ],
                      "back_days": 1,
                      "ahead_days": 7,
                      "schedule": "on_publish",
                      "every_min": 60,
                      "target": {
                        "host": "sftp.partner.example",
                        "port": 22,
                        "user": "viewstream",
                        "path": "/incoming/tv10.xml",
                        "host_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleExampleExampleExampleExampleExample"
                      },
                      "has_credentials": true,
                      "enabled": true,
                      "last_run_at": "2026-10-06T08:21:40Z",
                      "last_ok_at": "2026-10-06T08:21:40Z",
                      "last_status": "ok",
                      "created_at": "2026-10-01T07:35:00Z",
                      "updated_at": "2026-10-02T11:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Create a destination (a pull feed returns its token once)",
        "description": "**Required scope:** `epg:publish` or `channels:write`\n\nCreates a destination that publishes the tenant's guide to a partner: `pull` (a private feed URL\n/epg/d/<token>.xml|.json the partner polls; the token is returned once, in `token` and `feed_url`), `https`\n(POST, signed with X-VS-Signature like webhooks), `sftp` (written as.part, then renamed; host key pinned on\nfirst use unless given) or `s3`, or `eit` (DVB EIT for a playout / multiplexer: downloaded, or streamed over\nUDP/RTP; needs migration 0038). Push kinds deliver on `schedule`: `on_publish` (default; after each new version,\ndebounced a minute), `interval` (`every_min`) or `manual`; pull and eit are always `manual`. Push kinds need\ncredentials. Rules: `name` 1–120 characters, unique per tenant (409); `channels` 1–200 of the tenant's channels, no\nduplicates, `external_id` ≤ 200 characters without <>&\"'; `back_days` 0–14 (default 1); `ahead_days` 1–21 (default 7);\n`every_min` 5–1440 (default 60); `format` xmltv|json (default xmltv; eit for kind eit). Push targets pass the\nSSRF guard (public addresses only; an eit stream URL only via the operator allow-list EIT_STREAM_ALLOW).\n`credentials` are write-only and sealed at rest (https `secret`; sftp `password` or `private_key`; s3\n`access_key` + `secret_key`).\nScope `epg:publish` (or `channels:write`). Audited as `epg.destination.create`.",
        "operationId": "createEPGDestination",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EPGDestinationInput"
              },
              "example": {
                "name": "Partner Yes",
                "kind": "sftp",
                "format": "xmltv",
                "channels": [
                  {
                    "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                    "external_id": "tv10.yes.co.il"
                  }
                ],
                "ahead_days": 7,
                "schedule": "on_publish",
                "target": {
                  "host": "sftp.partner.example",
                  "user": "viewstream",
                  "path": "/incoming/tv10.xml"
                },
                "credentials": {
                  "password": "…"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created; a pull destination carries its `token` and full `feed_url` this once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGDestination"
                },
                "example": {
                  "id": "019a3c60-4b00-7c00-8d00-0000000000e1",
                  "customer_id": "019a3c00-0000-7000-8000-0000000000aa",
                  "name": "Partner pull feed",
                  "kind": "pull",
                  "format": "xmltv",
                  "channels": [
                    {
                      "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                      "external_id": "tv10.partner.example"
                    }
                  ],
                  "back_days": 1,
                  "ahead_days": 7,
                  "schedule": "manual",
                  "every_min": 60,
                  "target": {},
                  "has_credentials": false,
                  "token_hint": "epg_Q2x9vK…",
                  "feed_url": "https://api.viewstream.co.il/epg/d/epg_Q2x9vKpL3mN8rT1wZ5yB7cD0fG4hJ6kA.xml",
                  "token": "epg_Q2x9vKpL3mN8rT1wZ5yB7cD0fG4hJ6kA",
                  "enabled": true,
                  "created_at": "2026-10-06T10:30:00Z",
                  "updated_at": "2026-10-06T10:30:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 256 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "A destination with this name exists",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid destination (`errors[]` lists the fields: name, kind, format, channels — incl. \"no such channel\", days, schedule, target, missing credentials, SSRF-guard refusals)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`not_ready`: EPG publishing not available (migration 0027), eit destinations need migration 0038, or secret storage is not configured",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:publish or channels:write"
      }
    },
    "/v1/epg/destinations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "One destination (credentials are never returned)",
        "description": "**Required scope:** `channels:read`\n\nOne publishing destination of the tenant with its delivery state. Scope `channels:read`.",
        "operationId": "getEPGDestination",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id (of the caller's tenant; anything else = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Destination",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGDestination"
                },
                "example": {
                  "id": "019a3c60-4b00-7c00-8d00-0000000000e2",
                  "customer_id": "019a3c00-0000-7000-8000-0000000000aa",
                  "name": "Partner Yes",
                  "kind": "sftp",
                  "format": "xmltv",
                  "channels": [
                    {
                      "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                      "external_id": "tv10.yes.co.il"
                    }
                  ],
                  "back_days": 1,
                  "ahead_days": 7,
                  "schedule": "on_publish",
                  "every_min": 60,
                  "target": {
                    "host": "sftp.partner.example",
                    "port": 22,
                    "user": "viewstream",
                    "path": "/incoming/tv10.xml",
                    "host_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleExampleExampleExampleExampleExample"
                  },
                  "has_credentials": true,
                  "enabled": true,
                  "last_run_at": "2026-10-06T08:21:40Z",
                  "last_ok_at": "2026-10-06T08:21:40Z",
                  "last_status": "ok",
                  "created_at": "2026-10-01T07:35:00Z",
                  "updated_at": "2026-10-02T11:00:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      },
      "put": {
        "tags": [
          "epg"
        ],
        "summary": "Update a destination (omitted fields keep their value; omitted `credentials` keep the stored ones)",
        "description": "**Required scope:** `epg:publish` or `channels:write`\n\nPartial update: omitted fields keep their value, omitted `credentials` keep the stored ones (sent credentials\nreplace them). `kind` cannot change (422; create a new destination). A new `target` replaces the old one; an\nsftp pinned host key is kept unless the host changes or a new `host_key` is sent. For kind eit, `schedule` stays\nmanual and `ahead_days` follows `target.eit.schedule_days`. Rules: `name` 1–120 characters, unique per tenant (409); `channels` 1–200 of the tenant's channels, no\nduplicates, `external_id` ≤ 200 characters without <>&\"'; `back_days` 0–14 (default 1); `ahead_days` 1–21 (default 7);\n`every_min` 5–1440 (default 60); `format` xmltv|json (default xmltv; eit for kind eit). Push targets pass the\nSSRF guard (public addresses only; an eit stream URL only via the operator allow-list EIT_STREAM_ALLOW).\n`credentials` are write-only and sealed at rest (https `secret`; sftp `password` or `private_key`; s3\n`access_key` + `secret_key`).\nScope `epg:publish` (or `channels:write`). Audited as `epg.destination.update`.",
        "operationId": "putEPGDestination",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id (of the caller's tenant; anything else = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EPGDestinationInput"
              },
              "example": {
                "schedule": "interval",
                "every_min": 30,
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGDestination"
                },
                "example": {
                  "id": "019a3c60-4b00-7c00-8d00-0000000000e2",
                  "customer_id": "019a3c00-0000-7000-8000-0000000000aa",
                  "name": "Partner Yes",
                  "kind": "sftp",
                  "format": "xmltv",
                  "channels": [
                    {
                      "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                      "external_id": "tv10.yes.co.il"
                    }
                  ],
                  "back_days": 1,
                  "ahead_days": 7,
                  "schedule": "on_publish",
                  "every_min": 60,
                  "target": {
                    "host": "sftp.partner.example",
                    "port": 22,
                    "user": "viewstream",
                    "path": "/incoming/tv10.xml",
                    "host_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleExampleExampleExampleExampleExample"
                  },
                  "has_credentials": true,
                  "enabled": true,
                  "last_run_at": "2026-10-06T08:21:40Z",
                  "last_ok_at": "2026-10-06T08:21:40Z",
                  "last_status": "ok",
                  "created_at": "2026-10-01T07:35:00Z",
                  "updated_at": "2026-10-02T11:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields, or is larger than 256 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Another destination has this name",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid destination (`errors[]` lists the fields), including a changed `kind`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`not_ready`: EPG publishing not available (migration 0027), eit destinations need migration 0038, or secret storage is not configured",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:publish or channels:write"
      },
      "delete": {
        "tags": [
          "epg"
        ],
        "summary": "Delete a destination and its delivery log",
        "description": "**Required scope:** `epg:publish` or `channels:write`\n\nDeletes the destination and its delivery log; a pull feed URL stops working at once. Nothing is sent to the\npartner. Scope `epg:publish` (or `channels:write`). Audited as `epg.destination.delete`.",
        "operationId": "deleteEPGDestination",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id (of the caller's tenant; anything else = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:publish or channels:write"
      }
    },
    "/v1/epg/destinations/{id}/token": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Issue a new pull token (the old feed URL stops working); the token is returned once",
        "description": "**Required scope:** `epg:publish` or `channels:write`\n\nPull destinations only. Replaces the feed token: the old /epg/d/<token> URL answers 404 from now on; the new\ntoken and full `feed_url` are in this response only (store them). Scope `epg:publish` (or `channels:write`).\nAudited as `epg.destination.token`.",
        "operationId": "rotateEPGDestinationToken",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id (of the caller's tenant; anything else = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Destination with `token` and `feed_url`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGDestination"
                },
                "example": {
                  "id": "019a3c60-4b00-7c00-8d00-0000000000e1",
                  "customer_id": "019a3c00-0000-7000-8000-0000000000aa",
                  "name": "Partner pull feed",
                  "kind": "pull",
                  "format": "xmltv",
                  "channels": [
                    {
                      "channel_id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0c001",
                      "external_id": "tv10.partner.example"
                    }
                  ],
                  "back_days": 1,
                  "ahead_days": 7,
                  "schedule": "manual",
                  "every_min": 60,
                  "target": {},
                  "has_credentials": false,
                  "token_hint": "epg_Wm4tRb…",
                  "feed_url": "https://api.viewstream.co.il/epg/d/epg_Wm4tRbX7nQ2pL9sV3kY8zC1dF6gH0jA5.xml",
                  "token": "epg_Wm4tRbX7nQ2pL9sV3kY8zC1dF6gH0jA5",
                  "enabled": true,
                  "created_at": "2026-10-01T07:30:00Z",
                  "updated_at": "2026-10-06T10:40:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Only pull destinations have a token",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:publish or channels:write"
      }
    },
    "/v1/epg/destinations/{id}/run": {
      "post": {
        "tags": [
          "epg"
        ],
        "summary": "Deliver now (push) — or, for a pull feed or with `preview=1`, return the document without delivering",
        "description": "**Required scope:** `epg:publish` or `channels:write`\n\nPush destinations (https / sftp / s3): delivers the current guide now (trigger `manual`, up to 90 s) and returns\nthe delivery record — a failed push is still 200 with `status: failed` and `error`. With `preview=1`, and\nalways for a pull feed, returns the document (XMLTV or JSON, as the partner would get it) without delivering or\nlogging. An eit destination returns its EIT as TSDuck XML (attachment, logged as a manual delivery). Scope\n`epg:publish` (or `channels:write`). Push runs are audited as `epg.destination.run`.",
        "operationId": "runEPGDestination",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id (of the caller's tenant; anything else = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "preview",
            "in": "query",
            "description": "`1` or `true`: return the document instead of delivering",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The delivery (push), or the document (preview / pull: XMLTV or JSON feed; eit: TSDuck XML attachment)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/EPGDelivery"
                    },
                    {
                      "$ref": "#/components/schemas/EPGFeed"
                    }
                  ]
                },
                "example": {
                  "id": "019a3c70-5c00-7d00-8e00-0000000000d1",
                  "destination_id": "019a3c60-4b00-7c00-8d00-0000000000e2",
                  "trigger": "manual",
                  "status": "ok",
                  "programmes": 214,
                  "bytes": 96231,
                  "duration_ms": 812,
                  "started_at": "2026-10-06T10:45:00Z"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string"
                },
                "example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE tv SYSTEM \"xmltv.dtd\">\n<tv generator-info-name=\"ViewStream\">\n  <channel id=\"tv10.partner.example\">\n    <display-name>ערוץ 10</display-name>\n  </channel>\n  <programme start=\"20261006200000 +0300\" stop=\"20261006210000 +0300\" channel=\"tv10.partner.example\">\n    <title lang=\"he\">חדשות הערב</title>\n    <category lang=\"he\">חדשות</category>\n  </programme>\n</tv>\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`not_ready`: EPG publishing not available (migration 0027), or EPG delivery is not configured on this server",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "epg:publish or channels:write"
      }
    },
    "/v1/epg/destinations/{id}/deliveries": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Delivery log of a destination (newest 100)",
        "description": "**Required scope:** `channels:read`\n\nThe newest 100 deliveries of the destination: pushes (`schedule`, `publish`, `manual`), partner pulls (`pull`,\nlogged at most once a minute) and EIT downloads / stream cycles (`manual`, `stream`). The log keeps the newest\n500 runs per destination. Scope `channels:read`.",
        "operationId": "listEPGDeliveries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id (of the caller's tenant; anything else = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPGDelivery"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "019a3c70-5c00-7d00-8e00-0000000000d2",
                      "destination_id": "019a3c60-4b00-7c00-8d00-0000000000e2",
                      "trigger": "publish",
                      "status": "failed",
                      "programmes": 0,
                      "bytes": 0,
                      "duration_ms": 10012,
                      "error": "sftp: dial tcp: i/o timeout",
                      "started_at": "2026-10-06T08:21:40Z"
                    },
                    {
                      "id": "019a3c70-5c00-7d00-8e00-0000000000d1",
                      "destination_id": "019a3c60-4b00-7c00-8d00-0000000000e2",
                      "trigger": "manual",
                      "status": "ok",
                      "programmes": 214,
                      "bytes": 96231,
                      "duration_ms": 812,
                      "started_at": "2026-10-06T07:45:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/epg/destinations/{id}/eit": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "DVB EIT of an eit destination, rendered now",
        "description": "**Required scope:** `channels:read`\n\nDVB EIT of an eit destination, rendered now: a transport stream of EIT sections on PID 0x12 (format=ts, default) or TSDuck XML tables (format=xml)\n\nRenders the destination's EIT (EN 300 468 present/following + `schedule_days` of schedule, text in the configured\nencoding) from the published guide now and returns it as an attachment\n(`Content-Disposition: attachment; filename=\"eit-<name>-<YYYYMMDDTHHMMZ>.ts|xml\"`, `Cache-Control: no-store`).\nEach download is logged as a `manual` delivery. Scope `channels:read`.",
        "operationId": "downloadEPGDestinationEIT",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id (of the caller's tenant; anything else = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "`ts`: MPEG-TS packets (PID 0x12) for a multiplexer; `xml`: TSDuck XML tables (e.g. for tsp -I file / inspection)",
            "schema": {
              "type": "string",
              "enum": [
                "ts",
                "xml"
              ],
              "default": "ts"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The EIT (attachment)",
            "headers": {
              "Content-Disposition": {
                "description": "attachment; filename=\"eit-<destination name>-<UTC time>.<ts|xml>\"",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "video/mp2t": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string"
                },
                "example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- ViewStream DVB EIT, generated 2026-10-06 10:50:00 UTC; compile with: tstabcomp --default-charset ISO-8859-8 -->\n<tsduck>\n  <EIT type=\"pf\" version=\"3\" current=\"true\" actual=\"true\" service_id=\"101\" transport_stream_id=\"1\" original_network_id=\"1\" last_table_id=\"0x4E\" segment_last_section_number=\"1\">\n    <metadata PID=\"18\"/>\n    <event event_id=\"10753\" start_time=\"2026-10-06 17:00:00\" duration=\"01:00:00\" running_status=\"running\" CA_mode=\"false\">\n      <short_event_descriptor language_code=\"heb\">\n        <event_name>חדשות הערב</event_name>\n        <text></text>\n      </short_event_descriptor>\n    </event>\n  </EIT>\n</tsduck>\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Not a DVB EIT destination, or `format` not ts|xml",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/epg/destinations/{id}/eit/report": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Validation report of the EIT rendered now (CRC32, section lengths, numbering, event ids, Hebrew round trip)",
        "description": "**Required scope:** `channels:read`\n\nRenders the destination's EIT now and parses it back from its own transport stream: per-check results\n(`checks`; `ok` = all passed), the sub-tables, the present/following events and a sample of schedule events,\ncharacters the encoding could not carry (`lossy_characters`) and programmes or events left out. Nothing is\nlogged or sent. Scope `channels:read`.",
        "operationId": "reportEPGDestinationEIT",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id (of the caller's tenant; anything else = 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGEITReport"
                },
                "example": {
                  "at": "2026-10-06T10:50:00Z",
                  "encoding": "iso-8859-8",
                  "packets": 412,
                  "bytes": 77456,
                  "sections": 96,
                  "events": 188,
                  "lossy_characters": 0,
                  "skipped_programmes": 0,
                  "dropped_events": 0,
                  "ok": true,
                  "checks": [
                    {
                      "key": "ts",
                      "ok": true,
                      "detail": "412 packets on PID 0x0012, sync and continuity OK"
                    },
                    {
                      "key": "sections",
                      "ok": true,
                      "detail": "96 of 96 sections read back"
                    },
                    {
                      "key": "crc_length",
                      "ok": true,
                      "detail": "every CRC32 and section_length valid, all ≤ 4096 bytes"
                    },
                    {
                      "key": "numbering",
                      "ok": true,
                      "detail": "section numbers, segments and last_section_number consistent"
                    },
                    {
                      "key": "event_ids",
                      "ok": true,
                      "detail": "event_id unique per service"
                    },
                    {
                      "key": "text",
                      "ok": true,
                      "detail": "every event name decodes back (iso-8859-8)"
                    },
                    {
                      "key": "charset",
                      "ok": true,
                      "detail": "0 characters without a code in iso-8859-8 (shown as ?)"
                    }
                  ],
                  "tables": [
                    {
                      "table_id": "0x4E",
                      "service_id": 101,
                      "version": 3,
                      "sections": 2,
                      "events": 2,
                      "max_section_bytes": 184
                    },
                    {
                      "table_id": "0x50",
                      "service_id": 101,
                      "version": 3,
                      "sections": 32,
                      "events": 186,
                      "max_section_bytes": 4012
                    }
                  ],
                  "present_following": [
                    {
                      "event_id": 10753,
                      "start": "2026-10-06T17:00:00Z",
                      "duration": "1h0m0s",
                      "running_status": "running",
                      "names": {
                        "heb": "חדשות הערב"
                      },
                      "content": [
                        "0x20"
                      ]
                    }
                  ],
                  "sample": []
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Not a DVB EIT destination",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "EPG publishing is not available yet (`not_ready`, migration 0027)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/markers": {
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Manual programme boundary — closes the open programme at `at` and starts a new one",
        "description": "**Required scope:** `channels:write`\n\nEnds the programme that is open at `at` (default now) and inserts a new `manual` programme starting there with\nthe given title (open-ended). Emits `channel.programme_started` and is audited as `programme.marker`. Written\ndirectly, not through the EPG publishing workflow. Body ≤ 16 KB.",
        "operationId": "postMarker",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "default now"
                  },
                  "title": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "at": "2026-10-06T06:45:00Z",
                "title": "מבזק חדשות"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new programme",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Programme"
                },
                "example": {
                  "id": "01a11230-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
                  "channel_id": "0192c8a0-6c00-7c00-8c00-00000000c001",
                  "start_at": "2026-10-06T06:45:00Z",
                  "end_at": null,
                  "title": "מבזק חדשות",
                  "source": "manual",
                  "external_id": null,
                  "meta": null,
                  "created_at": "2026-10-06T06:45:01Z"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 16 KB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/clips": {
      "get": {
        "tags": [
          "clips"
        ],
        "summary": "List clips (newest first)",
        "description": "**Required scope:** `clips:read`\n\nThe tenant's clips, newest first, deleted ones included unless you filter by `status`. Pass `next_cursor`\nback as `cursor` until it is null. Scope `clips:read`; not for CDN-only tenants.",
        "operationId": "listClips",
        "parameters": [
          {
            "name": "channel_id",
            "in": "query",
            "description": "Only clips of this channel",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only clips in this state (not validated: an unknown value returns an empty list)",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "ready",
                "finalizing",
                "final",
                "failed",
                "deleted"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "`next_cursor` of the previous page (`<created_at RFC 3339>|<id>`); an unparsable cursor is ignored",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of clips",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "next_cursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Clip"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192c8a0-7d00-7d00-8d00-00000000d001",
                      "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "channel_id": "019a0000-0000-7000-8000-0000000000c1",
                      "asset_id": null,
                      "start_at": "2026-09-27T06:00:10Z",
                      "end_at": "2026-09-27T06:01:10Z",
                      "start_ms": null,
                      "end_ms": null,
                      "title": "Opening headlines",
                      "status": "ready",
                      "precision": "segment",
                      "renditions": [
                        "1080p",
                        "720p",
                        "540p",
                        "360p",
                        "aac-128"
                      ],
                      "finalize_job_id": null,
                      "provenance": {
                        "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                        "api_key": "vs_k3P9xQ2a"
                      },
                      "error": null,
                      "ready_at": "2026-09-27T06:05:00Z",
                      "final_at": null,
                      "created_at": "2026-09-27T06:05:00Z",
                      "updated_at": "2026-09-27T06:05:00Z",
                      "playback": {
                        "hls": "https://cdn.now14-poc.vustream.net/m/clips/0192c8a0-7d00-7d00-8d00-00000000d001/master.m3u8?c=now14poc"
                      },
                      "expires_at": "2026-10-04T06:01:10Z"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`limit` outside 1–200 or `channel_id` not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "clips:read"
      },
      "post": {
        "tags": [
          "clips"
        ],
        "summary": "Cut a clip from a channel recording",
        "description": "**Required scope:** `clips:write`\n\nCut a clip from a channel recording — playable immediately at segment precision; `precision: frame` also finalises\n\nCuts `start_at`–`end_at` (at most 6 h) out of a channel's recording. Each requested rendition must be recorded\nin the range; without `renditions` the clip takes every ladder rendition that was actually recorded there (at\nleast one). `segment` precision (default) is `ready` at once — its manifest points at the recording's\nsegments, so it expires with the recording (`expires_at` = end_at + the channel's retention). `frame`\nprecision starts `finalizing`: a `clip_finalize` job (fast priority) cuts exact frames into its own files and\nthe clip becomes `final` (watch `clip.status` events or `GET /v1/clips/{id}`). Only channel clips exist for\nnow (`asset_id` is refused). Body max 64 KB. Scope `clips:write` (`publish: true` needs `clips:publish`);\nnot for CDN-only tenants. Emits `clip.ready` and `clip.status`; audited as `clip.create`.",
        "operationId": "createClip",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClipInput"
              },
              "example": {
                "channel_id": "019a0000-0000-7000-8000-0000000000c1",
                "start_at": "2026-09-27T06:00:10Z",
                "end_at": "2026-09-27T06:01:10Z",
                "title": "Opening headlines",
                "precision": "segment"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The clip with its playback URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Clip"
                },
                "example": {
                  "id": "0192c8a0-7d00-7d00-8d00-00000000d001",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "channel_id": "019a0000-0000-7000-8000-0000000000c1",
                  "asset_id": null,
                  "start_at": "2026-09-27T06:00:10Z",
                  "end_at": "2026-09-27T06:01:10Z",
                  "start_ms": null,
                  "end_ms": null,
                  "title": "Opening headlines",
                  "status": "ready",
                  "precision": "segment",
                  "renditions": [
                    "1080p",
                    "720p",
                    "540p",
                    "360p",
                    "aac-128"
                  ],
                  "finalize_job_id": null,
                  "provenance": {
                    "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                    "api_key": "vs_k3P9xQ2a"
                  },
                  "error": null,
                  "ready_at": "2026-09-27T06:05:00Z",
                  "final_at": null,
                  "created_at": "2026-09-27T06:05:00Z",
                  "updated_at": "2026-09-27T06:05:00Z",
                  "playback": {
                    "hls": "https://cdn.now14-poc.vustream.net/m/clips/0192c8a0-7d00-7d00-8d00-00000000d001/master.m3u8?c=now14poc"
                  },
                  "expires_at": "2026-10-04T06:01:10Z"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, an unknown field or a body over 64 KB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing scope (`clips:write`; `clips:publish` for `publish: true`), CDN-only tenant, or suspended tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid clip: `precision`, `asset_id` given, missing channel_id/start_at/end_at, end not after start or over 6 h, unknown or deleted channel, unknown rendition, or nothing recorded in the range",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "invalid clip",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "errors": [
                    {
                      "field": "start_at",
                      "detail": "no recording in the requested range"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "clips:write"
      }
    },
    "/v1/clips/{id}": {
      "get": {
        "tags": [
          "clips"
        ],
        "summary": "One clip",
        "description": "**Required scope:** `clips:read`\n\nThe clip with its playback URL. `expires_at` is set for a `ready` (segment-precision) clip of a channel:\nthe moment its recording ages out. Deleted clips are still returned (status `deleted`; their manifests answer\n410). Scope `clips:read`; not for CDN-only tenants.",
        "operationId": "getClip",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Clip id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The clip",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Clip"
                },
                "example": {
                  "id": "0192c8a0-7d00-7d00-8d00-00000000d001",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "channel_id": "019a0000-0000-7000-8000-0000000000c1",
                  "asset_id": null,
                  "start_at": "2026-09-27T06:00:10Z",
                  "end_at": "2026-09-27T06:01:10Z",
                  "start_ms": null,
                  "end_ms": null,
                  "title": "Opening headlines",
                  "status": "ready",
                  "precision": "segment",
                  "renditions": [
                    "1080p",
                    "720p",
                    "540p",
                    "360p",
                    "aac-128"
                  ],
                  "finalize_job_id": null,
                  "provenance": {
                    "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                    "api_key": "vs_k3P9xQ2a"
                  },
                  "error": null,
                  "ready_at": "2026-09-27T06:05:00Z",
                  "final_at": null,
                  "created_at": "2026-09-27T06:05:00Z",
                  "updated_at": "2026-09-27T06:05:00Z",
                  "playback": {
                    "hls": "https://cdn.now14-poc.vustream.net/m/clips/0192c8a0-7d00-7d00-8d00-00000000d001/master.m3u8?c=now14poc"
                  },
                  "expires_at": "2026-10-04T06:01:10Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "clips:read"
      },
      "delete": {
        "tags": [
          "clips"
        ],
        "summary": "Delete a clip (its manifests answer 410 afterwards)",
        "description": "**Required scope:** `clips:write`\n\nMarks the clip `deleted` and queues an edge purge of its manifests so the CDN stops serving cached copies;\nthe manifest service then answers 410. The row stays visible in `GET /v1/clips` with status `deleted`;\ndeleting it again answers 204 again. Scope `clips:write`; not for CDN-only tenants. Emits `clip.status`;\naudited as `clip.delete`.",
        "operationId": "deleteClip",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Clip id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "clips:write"
      }
    },
    "/epg/{file}": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Public XMLTV guide of every channel of a tenant (`{tenant}.xml`), cacheable 5 min",
        "description": "Unauthenticated. The published guide of every channel of a platform tenant as one XMLTV document: programmes\nfrom each channel's retention window (at most 7 days back) to 14 days ahead, times in the guide time zone\n(GUIDE_TIMEZONE, Asia/Jerusalem), channel ids `<channel>.<tenant>.viewstream.co.il`. Public cache headers\n(5 min, CORS `*`). A tenant with `settings.epg_token` requires `?token=`; a wrong or missing token, an unknown\nor CDN-only tenant, or a file not ending in `.xml` all answer 404.",
        "operationId": "exportTenantEPG",
        "parameters": [
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "`<tenant slug>.xml`",
            "schema": {
              "type": "string",
              "example": "tv10poc.xml"
            }
          },
          {
            "name": "token",
            "in": "query",
            "description": "Required when the tenant made its guide private (`settings.epg_token`)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "XMLTV",
            "headers": {
              "Cache-Control": {
                "description": "public, max-age=300, stale-while-revalidate=600, stale-if-error=86400",
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "*",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                },
                "example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE tv SYSTEM \"xmltv.dtd\">\n<tv generator-info-name=\"ViewStream\">\n  <channel id=\"main.tv10poc.viewstream.co.il\">\n    <display-name>ערוץ 10</display-name>\n  </channel>\n  <programme start=\"20261006200000 +0300\" stop=\"20261006210000 +0300\" channel=\"main.tv10poc.viewstream.co.il\">\n    <title lang=\"he\">חדשות הערב</title>\n    <category lang=\"he\">חדשות</category>\n  </programme>\n</tv>\n"
              }
            }
          },
          "404": {
            "description": "No such guide (unknown or CDN-only tenant, wrong token, not `.xml`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/epg/d/{file}": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Per-partner pull feed of a pull destination",
        "description": "Per-partner pull feed of a pull destination — `<token>.xml` (XMLTV) or `<token>.json`; the destination's channels, ids and window\n\nUnauthenticated; the token in the path is the credential (see POST /v1/epg/destinations). Serves the enabled\npull destination's channels under the partner's channel ids, from `back_days` before today to `ahead_days`\nafter (guide time zone). The extension must match the destination's format. `Cache-Control: private,\nmax-age=60` (shared caches must not keep it). A pull is logged in the delivery log at most once a minute.\nUnknown, rotated or disabled token, wrong extension = 404.",
        "operationId": "exportEPGDestination",
        "parameters": [
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "`<token>.xml` or `<token>.json` (the destination's format)",
            "schema": {
              "type": "string",
              "example": "epg_Q2x9vKpL3mN8rT1wZ5yB7cD0fG4hJ6kA.xml"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "XMLTV or JSON (Cache-Control private, 60 s)",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                },
                "example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE tv SYSTEM \"xmltv.dtd\">\n<tv generator-info-name=\"ViewStream\">\n  <channel id=\"tv10.partner.example\">\n    <display-name>ערוץ 10</display-name>\n  </channel>\n  <programme start=\"20261006200000 +0300\" stop=\"20261006210000 +0300\" channel=\"tv10.partner.example\">\n    <title lang=\"he\">חדשות הערב</title>\n    <category lang=\"he\">חדשות</category>\n  </programme>\n</tv>\n"
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGFeed"
                },
                "example": {
                  "generator": "ViewStream",
                  "generated": "2026-10-06T10:55:00Z",
                  "from": "2026-10-04T21:00:00Z",
                  "to": "2026-10-12T21:00:00Z",
                  "channels": [
                    {
                      "id": "tv10.partner.example",
                      "slug": "main",
                      "name": "ערוץ 10",
                      "programmes": [
                        {
                          "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                          "start": "2026-10-06T17:00:00Z",
                          "stop": "2026-10-06T18:00:00Z",
                          "title": "חדשות הערב",
                          "meta": {
                            "lang": "he",
                            "category": "חדשות"
                          },
                          "catchup": true,
                          "start_over": "https://cdn.tv10-poc.vustream.net/m/startover/main/019a3c10-7d2e-7f41-9b0c-5e2a41c0d001/master.m3u8?c=tv10poc"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such guide (unknown, rotated or disabled token; wrong extension)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/epg/{tenant}/{file}": {
      "get": {
        "tags": [
          "epg"
        ],
        "summary": "Public guide of one channel — `<channel>.xml` (XMLTV) or `<channel>.json` (with start-over URLs)",
        "description": "Unauthenticated, same window, token rule and cache headers as the tenant guide. `.xml` is XMLTV; `.json` adds\nper programme the HLS `start_over` URL (omitted when rights forbid it or the programme is excluded from\ncatch-up), `catchup`, the rich `meta`, a `thumbnail` (smart poster or first recorded frame; null for future\nprogrammes) and, where programme boundaries are known, `play_start` / `play_stop` / `bounds_status`; the\nchannel carries a live `poster`. Unknown tenant / channel, wrong token or another extension = 404.",
        "operationId": "exportChannelEPG",
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "description": "Tenant slug",
            "schema": {
              "type": "string",
              "example": "tv10poc"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "`<channel slug>.xml` or `<channel slug>.json`",
            "schema": {
              "type": "string",
              "example": "main.xml"
            }
          },
          {
            "name": "token",
            "in": "query",
            "description": "Required when the tenant made its guide private (`settings.epg_token`)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "XMLTV or JSON",
            "headers": {
              "Cache-Control": {
                "description": "public, max-age=300, stale-while-revalidate=600, stale-if-error=86400",
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "*",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                },
                "example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE tv SYSTEM \"xmltv.dtd\">\n<tv generator-info-name=\"ViewStream\">\n  <channel id=\"main.tv10poc.viewstream.co.il\">\n    <display-name>ערוץ 10</display-name>\n  </channel>\n  <programme start=\"20261006200000 +0300\" stop=\"20261006210000 +0300\" channel=\"main.tv10poc.viewstream.co.il\">\n    <title lang=\"he\">חדשות הערב</title>\n    <category lang=\"he\">חדשות</category>\n  </programme>\n</tv>\n"
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EPGExportChannel"
                },
                "example": {
                  "id": "main.tv10poc.viewstream.co.il",
                  "slug": "main",
                  "name": "ערוץ 10",
                  "poster": "https://cdn.tv10-poc.vustream.net/rec/tv10poc/main/thumbs/latest.jpg",
                  "programmes": [
                    {
                      "id": "019a3c10-7d2e-7f41-9b0c-5e2a41c0d001",
                      "start": "2026-10-06T17:00:00Z",
                      "stop": "2026-10-06T18:00:00Z",
                      "title": "חדשות הערב",
                      "category": "חדשות",
                      "start_over": "https://cdn.tv10-poc.vustream.net/m/startover/main/019a3c10-7d2e-7f41-9b0c-5e2a41c0d001/r2/master.m3u8?c=tv10poc",
                      "catchup": true,
                      "meta": {
                        "lang": "he",
                        "category": "חדשות",
                        "rating": "all"
                      },
                      "thumbnail": "https://cdn.tv10-poc.vustream.net/vod/tv10poc/posters/programme/019a3c10-7d2e-7f41-9b0c-5e2a41c0d001/9c1e2f3a4b5d6e7f.jpg",
                      "play_start": "2026-10-06T17:00:42Z",
                      "play_stop": "2026-10-06T17:58:10Z",
                      "bounds_status": "detected"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such guide or channel (unknown or CDN-only tenant, wrong token, unknown channel, not `.xml`/`.json`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/player-configs": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player configurations of the tenant (the `default` first; created on first read)",
        "description": "**Required scope:** `assets:read`\n\nEvery player configuration (preset) of the tenant, the `default` first; the default is created from the tenant\nbranding the first time anything reads it. Each item carries the stored `config` normalised onto the current built-in\ndefaults, the `version` (bumped on every change) and the `public_url` of the document the player loads. `defaults` is\nthe built-in configuration that unset fields fall back to. Platform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "listPlayerConfigs",
        "responses": {
          "200": {
            "description": "Configs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlayerConfigView"
                      }
                    },
                    "defaults": {
                      "$ref": "#/components/schemas/PlayerConfigDocument"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192f0c4-1111-7000-8000-000000000001",
                      "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                      "name": "article",
                      "is_default": false,
                      "version": 3,
                      "updated_by": "key:ab12cd34",
                      "created_at": "2026-09-30T10:00:00Z",
                      "updated_at": "2026-10-06T08:00:00Z",
                      "public_url": "https://player.viewstream.co.il/player/config/tv10poc/article.json",
                      "config": {
                        "skin": "default",
                        "lang": "he",
                        "controls": {
                          "autoplay_muted": true,
                          "pip": true
                        },
                        "live": {
                          "epg_overlay": false
                        },
                        "ads": {
                          "enabled": false
                        }
                      }
                    }
                  ],
                  "defaults": {
                    "skin": "default",
                    "lang": "he",
                    "controls": {
                      "autoplay_muted": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "post": {
        "tags": [
          "players"
        ],
        "summary": "Create a named player configuration (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\n`name` must match `^[a-z0-9][a-z0-9-]{0,39}$` (`default` and `resolve` are reserved; unique per tenant, else 409).\n`config` (partial) is merged onto the tenant-branded default, or onto the existing config `from` (a copy); unknown keys\nand invalid values answer 422 with field paths. House pre-roll videos (`ads.house.asset_ids`) are checked to be ready,\nunprotected Library videos. `is_default: true` makes it the tenant default. Audited as `player_config.create`.\nPlatform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "createPlayerConfig",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,39}$"
                  },
                  "from": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Copy this config of the tenant as the base"
                  },
                  "is_default": {
                    "type": "boolean",
                    "default": false
                  },
                  "config": {
                    "$ref": "#/components/schemas/PlayerConfigDocument"
                  }
                }
              },
              "example": {
                "name": "article",
                "config": {
                  "controls": {
                    "autoplay_muted": true
                  },
                  "live": {
                    "epg_overlay": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerConfigView"
                },
                "example": {
                  "id": "0192f0c4-1111-7000-8000-000000000001",
                  "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                  "name": "article",
                  "is_default": false,
                  "version": 3,
                  "updated_by": "key:ab12cd34",
                  "created_at": "2026-09-30T10:00:00Z",
                  "updated_at": "2026-10-06T08:00:00Z",
                  "public_url": "https://player.viewstream.co.il/player/config/tv10poc/article.json",
                  "config": {
                    "skin": "default",
                    "lang": "he",
                    "controls": {
                      "autoplay_muted": true,
                      "pip": true
                    },
                    "live": {
                      "epg_overlay": false
                    },
                    "ads": {
                      "enabled": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "A config with that name exists (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid or reserved name (lower-case letters, digits, dashes, ≤ 40; `default` and `resolve` are reserved), unknown config keys or invalid values, or a house pre-roll video that is missing, not ready or protected; or `from` is not a config of the tenant (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/player-configs/{id}": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "One player configuration",
        "description": "**Required scope:** `assets:read`\n\nOne player configuration of the tenant, with `config` normalised onto the current defaults and its `public_url`.\nPlatform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "getPlayerConfig",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Player configuration id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Config",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerConfigView"
                },
                "example": {
                  "id": "0192f0c4-1111-7000-8000-000000000001",
                  "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                  "name": "article",
                  "is_default": false,
                  "version": 3,
                  "updated_by": "key:ab12cd34",
                  "created_at": "2026-09-30T10:00:00Z",
                  "updated_at": "2026-10-06T08:00:00Z",
                  "public_url": "https://player.viewstream.co.il/player/config/tv10poc/article.json",
                  "config": {
                    "skin": "default",
                    "lang": "he",
                    "controls": {
                      "autoplay_muted": true,
                      "pip": true
                    },
                    "live": {
                      "epg_overlay": false
                    },
                    "ads": {
                      "enabled": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such player config in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      },
      "patch": {
        "tags": [
          "players"
        ],
        "summary": "Change a configuration (partial `config` is merged); every change bumps `version` (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nPartial update: `name` (same rules as create), `config` (partial; merged onto the current values and re-validated) and\n`is_default: true` (makes it the default; the previous default is cleared). The default keeps its name and cannot be\nun-defaulted (`is_default: false` → 422). Every change bumps `version`; the public document changes within its 60 s\ncache. Audited as `player_config.update`. Platform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "patchPlayerConfig",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Player configuration id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,39}$"
                  },
                  "is_default": {
                    "type": "boolean",
                    "description": "Only `true` is accepted (switch the default)"
                  },
                  "config": {
                    "$ref": "#/components/schemas/PlayerConfigDocument"
                  }
                }
              },
              "example": {
                "config": {
                  "live": {
                    "epg_overlay": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerConfigView"
                },
                "example": {
                  "id": "0192f0c4-1111-7000-8000-000000000001",
                  "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                  "name": "article",
                  "is_default": false,
                  "version": 3,
                  "updated_by": "key:ab12cd34",
                  "created_at": "2026-09-30T10:00:00Z",
                  "updated_at": "2026-10-06T08:00:00Z",
                  "public_url": "https://player.viewstream.co.il/player/config/tv10poc/article.json",
                  "config": {
                    "skin": "default",
                    "lang": "he",
                    "controls": {
                      "autoplay_muted": true,
                      "pip": true
                    },
                    "live": {
                      "epg_overlay": false
                    },
                    "ads": {
                      "enabled": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such player config in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "A config with that name exists (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid or reserved name (lower-case letters, digits, dashes, ≤ 40; `default` and `resolve` are reserved), unknown config keys or invalid values, or a house pre-roll video that is missing, not ready or protected; renaming the default; un-defaulting the default (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      },
      "delete": {
        "tags": [
          "players"
        ],
        "summary": "Delete a named configuration (the default cannot be deleted — 409)",
        "description": "**Required scope:** `delivery:write`\n\nDeletes a named configuration. The tenant default cannot be deleted (409); make another config the default first.\nAudited as `player_config.delete`. Platform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "deletePlayerConfig",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Player configuration id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such player config in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The default config cannot be deleted (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/collections": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Library sections (top level first, then sub-sections; with item counts)",
        "description": "**Required scope:** `assets:read`\n\nEvery library section of the tenant in one list (no pagination): top-level sections first, then sub-sections,\neach group ordered by `sort` then `name`. `item_count` counts the assets placed directly in the section (not\nthose of its sub-sections). Sections nest one level deep. Not available to CDN-only tenants (403\n`feature_disabled`). Scope `assets:read`.",
        "operationId": "listCollections",
        "responses": {
          "200": {
            "description": "All sections of the tenant",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Collection"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                      "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "parent_id": null,
                      "name": "News",
                      "slug": "news",
                      "sort": 0,
                      "item_count": 42,
                      "created_at": "2026-09-28T08:12:03Z",
                      "updated_at": "2026-09-28T08:12:03Z"
                    },
                    {
                      "id": "0199b2c5-0a44-7d19-b3e0-5f7a1c3d8b22",
                      "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "parent_id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                      "name": "Evening news",
                      "slug": "evening-news",
                      "sort": 1,
                      "item_count": 17,
                      "created_at": "2026-09-28T08:13:40Z",
                      "updated_at": "2026-10-01T16:02:11Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      },
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Create a section (scope `assets:write`); one level of nesting — `parent_id` must be a top-level section",
        "description": "**Required scope:** `assets:write`\n\nCreates a library section. Without `slug` one is derived from the name (lowercase ASCII letters and digits,\nother characters folded to dashes, at most 60 characters; a name with no ASCII letters or digits — e.g. Hebrew\nonly — gets `section-<8 hex>`). The slug is unique per tenant (409). `parent_id` makes it a sub-section and must\nname a top-level section of the tenant. Unknown body fields are rejected (400). Audited as `collection.create`.\nNot available to CDN-only tenants. Scope `assets:write`.",
        "operationId": "createCollection",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "description": "Trimmed; 1–120 characters"
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
                    "description": "Derived from the name when omitted or empty"
                  },
                  "parent_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "A top-level section of the tenant; omit for a top-level section"
                  },
                  "sort": {
                    "type": "integer",
                    "default": 0,
                    "description": "Order among its siblings (ascending, then by name)"
                  }
                }
              },
              "example": {
                "name": "Evening news",
                "parent_id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                "sort": 1
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "id": "0199b2c5-0a44-7d19-b3e0-5f7a1c3d8b22",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "parent_id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                  "name": "Evening news",
                  "slug": "evening-news",
                  "sort": 1,
                  "item_count": 0,
                  "created_at": "2026-09-28T08:13:40Z",
                  "updated_at": "2026-09-28T08:13:40Z"
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 16 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "A section with that slug exists for this tenant (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed (`errors[]`: `name` 1–120 characters, `slug` pattern, `parent_id` unknown / not top-level)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/collections/{id}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "One section",
        "description": "**Required scope:** `assets:read`\n\nOne library section with its direct `item_count`. A section of another tenant, or an id that is not a UUID,\nanswers 404. List its assets with `GET /v1/assets?collection={id}` (add `children=true` for sub-sections).\nNot available to CDN-only tenants. Scope `assets:read`.",
        "operationId": "getCollection",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Section",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "parent_id": null,
                  "name": "News",
                  "slug": "news",
                  "sort": 0,
                  "item_count": 42,
                  "created_at": "2026-09-28T08:12:03Z",
                  "updated_at": "2026-09-28T08:12:03Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      },
      "patch": {
        "tags": [
          "library"
        ],
        "summary": "Rename / re-slug / reorder / move a section (`parent_id` null = top level)",
        "description": "**Required scope:** `assets:write`\n\nPartial update: only the fields present change. `parent_id: null` moves a sub-section to the top level; a\n`parent_id` must be another top-level section of the tenant, and a section that has sub-sections cannot become\na sub-section itself. Any other field is a 422 (`unknown field`). The slug stays unique per tenant (409).\nAudited as `collection.update`. Not available to CDN-only tenants. Scope `assets:write`.",
        "operationId": "patchCollection",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "description": "Trimmed; 1–120 characters"
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
                  },
                  "sort": {
                    "type": "integer"
                  },
                  "parent_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "A top-level section, or null to move to the top level"
                  }
                }
              },
              "example": {
                "name": "News (evening)",
                "sort": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated section",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "id": "0199b2c5-0a44-7d19-b3e0-5f7a1c3d8b22",
                  "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "parent_id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                  "name": "News (evening)",
                  "slug": "evening-news",
                  "sort": 2,
                  "item_count": 17,
                  "created_at": "2026-09-28T08:13:40Z",
                  "updated_at": "2026-10-06T09:41:12Z"
                }
              }
            }
          },
          "400": {
            "description": "Body is not a JSON object or is over 16 KiB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "A section with that slug exists for this tenant (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      },
      "delete": {
        "tags": [
          "library"
        ],
        "summary": "Delete a section with its sub-sections, memberships and section preset assignments (assets are kept)",
        "description": "**Required scope:** `assets:write`\n\nDeletes the section, its sub-sections, their asset memberships and any player preset assignments that target\nthem (scope `collection`); the assets themselves are not touched. Not reversible. Audited as\n`collection.delete`. Not available to CDN-only tenants. Scope `assets:write`.",
        "operationId": "deleteCollection",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/collections/{id}/items": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Add assets to a section (idempotent)",
        "description": "**Required scope:** `assets:write`\n\nAdds up to 500 assets to the section, appended after its current last position. Assets already in the\nsection are skipped, so repeating the call is harmless; `changed` counts the memberships actually added. An\nasset may be in several sections. Every id must be a non-deleted asset of the tenant, otherwise nothing changes\nand 422 lists the bad `asset_ids[i]`. Audited as `collection.items.add`.\nNot available to CDN-only tenants. Scope `assets:write`.",
        "operationId": "addCollectionItems",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "asset_ids"
                ],
                "additionalProperties": false,
                "properties": {
                  "asset_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "minItems": 1,
                    "maxItems": 500,
                    "description": "Assets of the tenant that are not deleted"
                  }
                }
              },
              "example": {
                "asset_ids": [
                  "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                  "0192c8b1-2a7d-7c40-8e15-6b9f0a1c2d3e"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The section after the change and how many memberships were added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "collection",
                    "changed"
                  ],
                  "properties": {
                    "collection": {
                      "$ref": "#/components/schemas/Collection"
                    },
                    "changed": {
                      "type": "integer",
                      "description": "Memberships added (assets already in the section are not counted)"
                    }
                  }
                },
                "example": {
                  "collection": {
                    "id": "0199b2c5-0a44-7d19-b3e0-5f7a1c3d8b22",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "parent_id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                    "name": "Evening news",
                    "slug": "evening-news",
                    "sort": 1,
                    "item_count": 19,
                    "created_at": "2026-09-28T08:13:40Z",
                    "updated_at": "2026-10-01T16:02:11Z"
                  },
                  "changed": 2
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 64 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Validation failed: `asset_ids` empty or over 500, or `asset_ids[i]` is not an asset of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/collections/{id}/items/remove": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Remove assets from a section",
        "description": "**Required scope:** `assets:write`\n\nRemoves up to 500 assets from the section (the assets themselves stay in the library). Ids that are not in\nthe section are ignored; `changed` counts the memberships removed. Every id must still be a non-deleted asset\nof the tenant (422 otherwise). Audited as `collection.items.remove`.\nNot available to CDN-only tenants. Scope `assets:write`.",
        "operationId": "removeCollectionItems",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "asset_ids"
                ],
                "additionalProperties": false,
                "properties": {
                  "asset_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "minItems": 1,
                    "maxItems": 500,
                    "description": "Assets of the tenant that are not deleted"
                  }
                }
              },
              "example": {
                "asset_ids": [
                  "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                  "0192c8b1-2a7d-7c40-8e15-6b9f0a1c2d3e"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The section after the change and how many memberships were removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "collection",
                    "changed"
                  ],
                  "properties": {
                    "collection": {
                      "$ref": "#/components/schemas/Collection"
                    },
                    "changed": {
                      "type": "integer",
                      "description": "Memberships removed"
                    }
                  }
                },
                "example": {
                  "collection": {
                    "id": "0199b2c5-0a44-7d19-b3e0-5f7a1c3d8b22",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "parent_id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                    "name": "Evening news",
                    "slug": "evening-news",
                    "sort": 1,
                    "item_count": 15,
                    "created_at": "2026-09-28T08:13:40Z",
                    "updated_at": "2026-10-01T16:02:11Z"
                  },
                  "changed": 2
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 64 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Validation failed: `asset_ids` empty or over 500, or `asset_ids[i]` is not an asset of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/collections/{id}/items/move": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Move assets from this section to section `to`",
        "description": "**Required scope:** `assets:write`\n\nAdds the assets to section `to` (idempotent, appended at its end) and then removes them from this section.\n`changed` is the number of memberships removed from this section (0 when `to` is this section). `to` can be\nany section of the tenant, top level or sub-section. Every id must be a non-deleted asset of the tenant (422\notherwise). Audited as `collection.items.move`.\nNot available to CDN-only tenants. Scope `assets:write`.",
        "operationId": "moveCollectionItems",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Section id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "asset_ids",
                  "to"
                ],
                "additionalProperties": false,
                "properties": {
                  "asset_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "minItems": 1,
                    "maxItems": 500,
                    "description": "Assets of the tenant that are not deleted"
                  },
                  "to": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The target section (of the same tenant)"
                  }
                }
              },
              "example": {
                "asset_ids": [
                  "0192c8a0-4f1e-7a2b-9c3d-1a2b3c4d5e6f",
                  "0192c8b1-2a7d-7c40-8e15-6b9f0a1c2d3e"
                ],
                "to": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "This (source) section after the change and how many memberships left it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "collection",
                    "changed"
                  ],
                  "properties": {
                    "collection": {
                      "$ref": "#/components/schemas/Collection"
                    },
                    "changed": {
                      "type": "integer",
                      "description": "Memberships removed from this section"
                    }
                  }
                },
                "example": {
                  "collection": {
                    "id": "0199b2c5-0a44-7d19-b3e0-5f7a1c3d8b22",
                    "customer_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                    "parent_id": "0199b2c4-7e10-7a3c-8f21-4b6d0c2e9a11",
                    "name": "Evening news",
                    "slug": "evening-news",
                    "sort": 1,
                    "item_count": 15,
                    "created_at": "2026-09-28T08:13:40Z",
                    "updated_at": "2026-10-01T16:02:11Z"
                  },
                  "changed": 2
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, is over 64 KiB or has an unknown field (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Validation failed: `asset_ids` empty or over 500, `asset_ids[i]` not an asset of the tenant, or `to` missing / not a section of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:write"
      }
    },
    "/v1/player-config-assignments": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player preset assignments (the tenant default is listed as scope `tenant`)",
        "description": "**Required scope:** `assets:read`\n\nEvery preset assignment of the tenant. The first item is always the tenant default as a pseudo-assignment\n(`scope: tenant`, `id: null`); the default config is created if missing. Not paginated. Platform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "listPresetAssignments",
        "responses": {
          "200": {
            "description": "Assignments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PresetAssignment"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": null,
                      "scope": "tenant",
                      "target_id": null,
                      "config_id": "0192f0c4-1111-7000-8000-000000000000",
                      "config_name": "default",
                      "updated_by": null,
                      "updated_at": "2026-09-30T10:00:00Z"
                    },
                    {
                      "id": "0192f0c5-2222-7000-8000-000000000001",
                      "scope": "collection",
                      "target_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                      "config_id": "0192f0c4-1111-7000-8000-000000000002",
                      "config_name": "series",
                      "updated_by": "key:ab12cd34",
                      "updated_at": "2026-10-06T08:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      },
      "post": {
        "tags": [
          "players"
        ],
        "summary": "Assign a preset to a scope (upsert by scope + target; scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nUpserts the assignment for (`scope`, `target_id`). Scopes: `tenant` (makes the config the tenant default),\n`live_default`, `library_default` (no target), `channel`, `collection` (a Library section), `asset` (target required,\nmust belong to the tenant). Resolution: VOD — asset, section (sub-section, then parent), library default, tenant;\nlive — channel, live default, tenant; clip — as its source asset, else channel, library default, tenant. Answers 200\n(also for a new assignment). Audited as `player_preset.assign`. Platform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "upsertPresetAssignment",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scope",
                  "config_id"
                ],
                "properties": {
                  "scope": {
                    "type": "string",
                    "enum": [
                      "tenant",
                      "live_default",
                      "library_default",
                      "channel",
                      "collection",
                      "asset"
                    ]
                  },
                  "target_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Required for channel, collection, asset; must be null/absent otherwise"
                  },
                  "config_id": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              },
              "example": {
                "scope": "collection",
                "target_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                "config_id": "0192f0c4-1111-7000-8000-000000000002"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresetAssignment"
                },
                "example": {
                  "id": "0192f0c5-2222-7000-8000-000000000001",
                  "scope": "collection",
                  "target_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                  "config_id": "0192f0c4-1111-7000-8000-000000000002",
                  "config_name": "series",
                  "updated_by": "key:ab12cd34",
                  "updated_at": "2026-10-06T08:00:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Unknown scope, missing/extra/foreign target, or a config_id that is not a config of the tenant (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/player-config-assignments/{id}": {
      "delete": {
        "tags": [
          "players"
        ],
        "summary": "Remove an assignment (content falls back along the chain)",
        "description": "**Required scope:** `delivery:write`\n\nDeletes one assignment; the content it covered falls back along the resolution chain. The tenant default\npseudo-assignment has no id and cannot be deleted (assign another config to scope `tenant` instead). Audited as\n`player_preset.unassign`. Platform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "deletePresetAssignment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Assignment id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such assignment in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/player-config-assignments/resolve": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Which preset applies to a piece of content and where it is inherited from (Studio)",
        "description": "**Required scope:** `assets:read`\n\nRuns the same resolution as the public `resolve.json` for the content and returns where the preset comes from (Studio's\n\"inherited from …\") and the public URL of the resolved document. Platform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "resolvePreset",
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "live",
                "vod",
                "clip"
              ]
            }
          },
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "live: channel slug; vod: asset id; clip: clip id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolution",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "resolved_from": {
                      "$ref": "#/components/schemas/ResolvedFrom"
                    },
                    "public_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                },
                "example": {
                  "resolved_from": {
                    "scope": "collection",
                    "target_id": "01a0e79f-a725-79e5-80d8-ff1366a12076",
                    "target_name": "Drama",
                    "config_id": "0192f0c4-1111-7000-8000-000000000002",
                    "config_name": "series"
                  },
                  "public_url": "https://player.viewstream.co.il/player/config/tv10poc/resolve.json?kind=vod&id=0192a1b2-0000-7000-8000-000000000001"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown kind, or no such content in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "assets:read"
      }
    },
    "/v1/stats/realtime": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Realtime numbers — the same payload the `stats.realtime` SSE event carries every 10 s",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. The latest snapshot insight published for the tenant (concurrent viewers live / VOD, plays in\nthe last 5 minutes and hour, delivered Gbps per edge site, top channels and assets, fatal errors per minute).\nSSE is canonical for the Dashboard; this route is the fallback polled while the stream is down.\nBefore the first snapshot it answers all zeros with `at` = now. `Cache-Control: private, max-age=5`. Available\nto CDN-only tenants. No query parameters.",
        "operationId": "statsRealtime",
        "responses": {
          "200": {
            "description": "Snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsRealtime"
                },
                "example": {
                  "concurrent": {
                    "live": 412,
                    "vod": 57,
                    "total": 469
                  },
                  "plays_last_5m": 96,
                  "plays_last_hour": 1043,
                  "gbps": {
                    "total": 1.84,
                    "by_site": {
                      "med1": 1.21,
                      "fornax": 0.63
                    }
                  },
                  "top_channels": [
                    {
                      "id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "slug": "main",
                      "title": "ערוץ 14",
                      "concurrent": 398
                    }
                  ],
                  "top_assets": [
                    {
                      "id": "01928f10-6a2d-7e41-8c3b-5d9e1f2a3b4c",
                      "title": "המהדורה המרכזית",
                      "concurrent": 21
                    }
                  ],
                  "fatal_errors_per_min": 0,
                  "at": "2026-10-06T08:15:10Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/overview": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Period overview with a comparison period",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Plays, attempts, EBVS (exits before video start), viewers, watch time, completion, peak\nconcurrency, startup percentiles, rebuffer ratio, error rate, bitrate, ad figures and delivered bytes for the\nrange and for a comparison window: `compare` = previous (default: the span of the same length right before),\nyesterday (the same clock span 24 h earlier) or last_week (7 days earlier); the response echoes `compare`.\nRange: `from`/`to` default to the last 24 h, `from` must be before `to`, at most 2 years. Filters a table cannot\napply are ignored (the audience rollups take country, device, pathway and one channel/asset/clip; the session\nfigures also take live and page_host). Answers are cached 30 s per tenant and parameters\n(`Cache-Control: private, max-age=30`).",
        "operationId": "statsOverview",
        "parameters": [
          {
            "name": "compare",
            "in": "query",
            "description": "Comparison window (default previous)",
            "schema": {
              "type": "string",
              "enum": [
                "previous",
                "yesterday",
                "last_week"
              ],
              "default": "previous"
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsPathway"
          },
          {
            "$ref": "#/components/parameters/StatsLive"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          },
          {
            "$ref": "#/components/parameters/StatsClip"
          },
          {
            "$ref": "#/components/parameters/StatsPageHost"
          }
        ],
        "responses": {
          "200": {
            "description": "Overview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsOverview"
                },
                "example": {
                  "from": "2026-09-29T00:00:00Z",
                  "to": "2026-09-30T00:00:00Z",
                  "compare": "previous",
                  "current": {
                    "plays": 1200,
                    "attempts": 1310,
                    "ebvs_pct": 8.4,
                    "viewers": 640,
                    "watch_time_ms": 43200000,
                    "avg_view_duration_ms": 36000,
                    "completion_rate": 0.41,
                    "peak_concurrent": 85,
                    "peak_at": "2026-09-29T19:30:00Z",
                    "startup_p50_ms": 450,
                    "startup_p95_ms": 1300,
                    "rebuffer_ratio": 0.002,
                    "error_rate": 0.004,
                    "avg_bitrate_kbps": 3400,
                    "ad_impressions": 0,
                    "ad_completion_rate": 0,
                    "bytes_delivered": 120000000000,
                    "vid_coverage": 0.86,
                    "plays_no_consent": 74
                  },
                  "previous": {
                    "plays": 1100,
                    "attempts": 1220,
                    "ebvs_pct": 9.8,
                    "viewers": 600,
                    "watch_time_ms": 40100000,
                    "avg_view_duration_ms": 36400,
                    "completion_rate": 0.39,
                    "peak_concurrent": 80,
                    "peak_at": "2026-09-28T19:45:00Z",
                    "startup_p50_ms": 470,
                    "startup_p95_ms": 1500,
                    "rebuffer_ratio": 0.0025,
                    "error_rate": 0.005,
                    "avg_bitrate_kbps": 3300,
                    "ad_impressions": 0,
                    "ad_completion_rate": 0,
                    "bytes_delivered": 110000000000,
                    "vid_coverage": 0.84,
                    "plays_no_consent": 69
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/timeseries": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "One metric over time",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. `metric` ∈ plays, attempts, viewers, plays_no_consent, watch_time, rebuffer_ratio, error_rate,\nstartup_avg, ad_impressions (audience rollups); concurrent; bytes, gbps, requests, cache_hit_ratio, origin_bytes\n(edge traffic, the origin head's own rows excluded). `interval` 1m (range ≤ 24 h), 1h (≤ 90 days), 1d (≤ 2 years);\ndefault 1m up to 24 h, 1h up to 90 days, else 1d. `group_by` depends on the metric's table: audience → country,\ndevice, pathway, channel, asset; traffic → country, site, edge, path_type, host, cache_status, status_class;\nconcurrent → channel, asset, pathway. `compare` adds the same metric for the comparison window as `previous`,\neach point shifted forward by `offset_ms`. An unknown metric, interval or group_by, or a range too long for the\ninterval, is 422. Cached 30 s.",
        "operationId": "statsTimeseries",
        "parameters": [
          {
            "name": "metric",
            "in": "query",
            "required": true,
            "description": "The metric",
            "schema": {
              "type": "string",
              "enum": [
                "plays",
                "attempts",
                "viewers",
                "plays_no_consent",
                "watch_time",
                "rebuffer_ratio",
                "error_rate",
                "startup_avg",
                "ad_impressions",
                "concurrent",
                "bytes",
                "gbps",
                "requests",
                "cache_hit_ratio",
                "origin_bytes"
              ]
            }
          },
          {
            "name": "interval",
            "in": "query",
            "description": "Bucket size; default by range (1m ≤ 24 h, 1h ≤ 90 days, else 1d)",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "1h",
                "1d"
              ]
            }
          },
          {
            "name": "group_by",
            "in": "query",
            "description": "One series per value of this dimension (allowed values depend on the metric, see above)",
            "schema": {
              "type": "string",
              "enum": [
                "country",
                "device",
                "pathway",
                "channel",
                "asset",
                "site",
                "edge",
                "path_type",
                "host",
                "cache_status",
                "status_class"
              ]
            }
          },
          {
            "name": "compare",
            "in": "query",
            "description": "also return the same metric for the comparison window (`previous` = the span right before, `yesterday` / `last_week` = the same clock span 1 / 7 days earlier) as `previous`, each point shifted forward by `offset_ms` so it overlays the range",
            "schema": {
              "type": "string",
              "enum": [
                "previous",
                "yesterday",
                "last_week"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsPathway"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          },
          {
            "$ref": "#/components/parameters/StatsClip"
          }
        ],
        "responses": {
          "200": {
            "description": "Series",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsSeries"
                },
                "example": {
                  "metric": "plays",
                  "interval": "1h",
                  "points": [
                    {
                      "t": "2026-10-05T18:00:00Z",
                      "value": 182
                    },
                    {
                      "t": "2026-10-05T19:00:00Z",
                      "value": 240
                    }
                  ],
                  "compare": "yesterday",
                  "previous_from": "2026-10-04T18:00:00Z",
                  "previous_to": "2026-10-04T20:00:00Z",
                  "offset_ms": 86400000,
                  "previous": [
                    {
                      "t": "2026-10-05T18:00:00Z",
                      "value": 171
                    },
                    {
                      "t": "2026-10-05T19:00:00Z",
                      "value": 226
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/breakdown": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Sessions grouped by a dimension",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Groups the sessions (by session start) by `dimension` and ranks the values by `metric`, with\neach value's `share` of the listed total. Viewer attributes (age_band, gender, tier, segment) only count sessions\nthat carry the attribute and also return `coverage` (share of plays that carry it). `programme` (insight-v2 1.1)\ngroups by the EPG programme the session watched — live: on air at the session start; catch-up / start-over: the\nprogramme the URL names; programme VODs: the programme they were published from; `key` '' = not attributed — and\neach row carries `title`, `start` and `channel`. `programme=<id>` narrows to one programme (422 when not a UUID).\nAll session filters apply. `limit` 1–500, default 50 (out-of-range values fall back to 50). Cached 30 s.",
        "operationId": "statsBreakdown",
        "parameters": [
          {
            "name": "dimension",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "country",
                "region",
                "city",
                "isp",
                "asn",
                "device",
                "os",
                "browser",
                "player_ver",
                "engine",
                "embed",
                "page_host",
                "referrer_host",
                "pathway",
                "hour_of_day",
                "day_of_week",
                "age_band",
                "gender",
                "tier",
                "segment",
                "channel",
                "asset",
                "bitrate_band",
                "live_vod",
                "programme"
              ]
            },
            "description": "Grouping; hour_of_day is Israel time, day_of_week 1 = Monday"
          },
          {
            "name": "metric",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "plays",
                "viewers",
                "plays_no_consent",
                "watch_time",
                "startup_p95",
                "rebuffer_ratio",
                "error_rate",
                "avg_bitrate",
                "sessions",
                "errors",
                "rebuffers",
                "completion"
              ],
              "default": "plays"
            },
            "description": "Ranking metric"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Rows (1–500)",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsPathway"
          },
          {
            "$ref": "#/components/parameters/StatsLive"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          },
          {
            "$ref": "#/components/parameters/StatsClip"
          },
          {
            "$ref": "#/components/parameters/StatsPageHost"
          },
          {
            "$ref": "#/components/parameters/StatsProgramme"
          }
        ],
        "responses": {
          "200": {
            "description": "Rows",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsBreakdown"
                },
                "example": {
                  "dimension": "device",
                  "metric": "plays",
                  "rows": [
                    {
                      "key": "phone",
                      "value": 812,
                      "share": 0.62
                    },
                    {
                      "key": "desktop",
                      "value": 341,
                      "share": 0.26
                    },
                    {
                      "key": "tv",
                      "value": 157,
                      "share": 0.12
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/top": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Top assets, clips or channels",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Ranks the tenant's assets, clips or channels by plays, watch time, viewers or plays without\nconsent (from the sessions; session filters apply) or by delivered `bytes` (daily per-asset traffic, whole days of\nthe range; other filters do not apply). `id` is the asset/clip id or the channel slug; `title` is filled from the\ncatalogue when known. `limit` 1–100, default 10. Cached 30 s.",
        "operationId": "statsTop",
        "parameters": [
          {
            "name": "entity",
            "in": "query",
            "required": true,
            "description": "What to rank",
            "schema": {
              "type": "string",
              "enum": [
                "assets",
                "clips",
                "channels"
              ]
            }
          },
          {
            "name": "metric",
            "in": "query",
            "description": "Ranking metric",
            "schema": {
              "type": "string",
              "enum": [
                "plays",
                "watch_time",
                "viewers",
                "plays_no_consent",
                "bytes"
              ],
              "default": "plays"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items (1–100)",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsLive"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsPageHost"
          }
        ],
        "responses": {
          "200": {
            "description": "Ranking",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsTop"
                },
                "example": {
                  "entity": "assets",
                  "metric": "plays",
                  "items": [
                    {
                      "id": "01928f10-6a2d-7e41-8c3b-5d9e1f2a3b4c",
                      "title": "המהדורה המרכזית 05.10",
                      "value": 384
                    },
                    {
                      "id": "01928f22-0b7c-7a19-9e4d-2f6a8c1b3d5e",
                      "title": "פאנל הבוקר",
                      "value": 211
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/traffic": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Edge traffic from the access logs — requests, bytes, Gbps, hit ratios, origin egress",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`; available to CDN-only tenants. Aggregates the edges' access logs (1-minute rollup, 1-hour\nbeyond 89 days). Delivered requests, bytes, Gbps and hit ratios exclude the origin head's own log rows;\n`origin_bytes` is the origin egress. Without `interval` there is one row per group for the whole range (`gbps`\naveraged over the range); with it, one row per bucket and group (`t` set). Only the `country` filter applies.\nCached 30 s.",
        "operationId": "statsTraffic",
        "parameters": [
          {
            "name": "group_by",
            "in": "query",
            "description": "Group rows by this dimension (none = one total row)",
            "schema": {
              "type": "string",
              "enum": [
                "host",
                "site",
                "edge",
                "path_type",
                "cache_status",
                "status_class",
                "country"
              ]
            }
          },
          {
            "name": "interval",
            "in": "query",
            "description": "Bucket size (1m ≤ 24 h, 1h ≤ 90 days, 1d ≤ 2 years); omitted = whole range",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "1h",
                "1d"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          }
        ],
        "responses": {
          "200": {
            "description": "Traffic",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsTraffic"
                },
                "example": {
                  "group_by": "site",
                  "rows": [
                    {
                      "group": "med1",
                      "requests": 18420311,
                      "bytes": 9876543210987,
                      "gbps": 0.914,
                      "hit_ratio_requests": 0.982,
                      "hit_ratio_bytes": 0.991,
                      "origin_bytes": 0
                    },
                    {
                      "group": "fornax",
                      "requests": 7310442,
                      "bytes": 4012345678901,
                      "gbps": 0.371,
                      "hit_ratio_requests": 0.975,
                      "hit_ratio_bytes": 0.987,
                      "origin_bytes": 0
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/traffic/percentile": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "95th percentile of delivered Gbps (5-minute samples; 1-hour beyond the 1m retention), peak and mean",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`; available to CDN-only tenants. Delivered Gbps per 5-minute sample (1-hour samples when the\nrange starts more than 89 days ago; `sample_secs` says which), its 95th percentile (the usual transit billing\nfigure), the peak and the mean over the range. Only the `country` filter applies. Cached 30 s.",
        "operationId": "statsTrafficPercentile",
        "parameters": [
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          }
        ],
        "responses": {
          "200": {
            "description": "Percentile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsTrafficPercentile"
                },
                "example": {
                  "from": "2026-09-01T00:00:00Z",
                  "to": "2026-10-01T00:00:00Z",
                  "sample_secs": 300,
                  "samples": 8640,
                  "p95_gbps": 2.41,
                  "peak_gbps": 3.07,
                  "mean_gbps": 0.88
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/viewers/recurrence": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "New vs returning viewers in the period (lookback up to 89 days; approximate when few sessions carry a vid)",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Only a stable anonymous viewer id can be new or returning: returning = it played in the\n`lookback_days` before the range or in more than one session inside it; new = the other stable ids. Viewers\nwithout a stable id (per-page-load ids, fingerprints of old players) are `without_vid`, so new + returning +\nwithout_vid = viewers. Plays without consent are counted separately and never as viewers. `approximate` is true\nwhen fewer than 80 % of plays carry a stable id. `lookback_days` 1–89, default 30 (other values fall back to 30).\nSession filters apply. Cached 30 s.",
        "operationId": "statsRecurrence",
        "parameters": [
          {
            "name": "lookback_days",
            "in": "query",
            "description": "Days before the range that make a viewer \"returning\"",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 89,
              "default": 30
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          }
        ],
        "responses": {
          "200": {
            "description": "Recurrence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsRecurrence"
                },
                "example": {
                  "from": "2026-09-29T00:00:00Z",
                  "to": "2026-10-06T00:00:00Z",
                  "lookback_days": 30,
                  "viewers": 5120,
                  "new": 1840,
                  "returning": 2710,
                  "without_vid": 570,
                  "plays_no_consent": 312,
                  "vid_coverage": 0.89,
                  "approximate": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/protection": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Edge denials (X-VS-Deny) by reason, country and ASN, with a per-reason timeseries",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Requests the edges refused for the tenant (expired or invalid token, geo, referrer, blackout…)\nfrom the per-minute denial rollup: the top reasons, countries and ASNs by requests (each with bytes), the total,\nand a per-reason series at `interval` (default 1h; 1m ≤ 24 h, 1h ≤ 90 days, 1d ≤ 2 years). `limit` 1–200 rows per\ngroup, default 20. Only the `country` filter applies. Cached 30 s.",
        "operationId": "statsProtection",
        "parameters": [
          {
            "name": "interval",
            "in": "query",
            "description": "Series bucket size",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "1h",
                "1d"
              ],
              "default": "1h"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Rows per group",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 20
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          }
        ],
        "responses": {
          "200": {
            "description": "Denials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsDenials"
                },
                "example": {
                  "from": "2026-10-05T00:00:00Z",
                  "to": "2026-10-06T00:00:00Z",
                  "interval": "1h",
                  "total": 1532,
                  "reasons": [
                    {
                      "key": "token_expired",
                      "requests": 1210,
                      "bytes": 386000
                    },
                    {
                      "key": "geo",
                      "requests": 322,
                      "bytes": 98100
                    }
                  ],
                  "countries": [
                    {
                      "key": "IL",
                      "requests": 1190,
                      "bytes": 371000
                    },
                    {
                      "key": "DE",
                      "requests": 342,
                      "bytes": 113100
                    }
                  ],
                  "asns": [
                    {
                      "key": "8551",
                      "requests": 640,
                      "bytes": 201000
                    }
                  ],
                  "series": [
                    {
                      "t": "2026-10-05T19:00:00Z",
                      "reason": "token_expired",
                      "requests": 88
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/sites": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "a site's page views, visitors, top pages, section CTR, rail click-through by tile position, search terms…",
        "description": "**Required scope:** `stats:read`\n\na site's page views, visitors, top pages, section CTR, rail click-through by tile position, search terms, referrers and 404s from the site events\n\nScope `stats:read`. Reads the site events of every hostname of the site (`site` must be a site of the tenant):\ntotals, a page-view series at `interval` (default 1h), top pages, sections with CTR, click-through by tile\nposition (`position_unknown` = clicks from builds that sent no position), search terms with zero-result counts,\nother-site referrers and the most requested missing paths (`not_found_pages`; `not_found_since` is null until the\nrenderer has reported a 404, so \"no data yet\" differs from \"no 404s\"). `limit` 1–200 rows per list, default 20.\nA site without a hostname is 422. Cached 30 s.",
        "operationId": "statsSites",
        "parameters": [
          {
            "name": "site",
            "in": "query",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "interval",
            "in": "query",
            "description": "Series bucket size (1m ≤ 24 h, 1h ≤ 90 days, 1d ≤ 2 years)",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "1h",
                "1d"
              ],
              "default": "1h"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Rows per list",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 20
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          }
        ],
        "responses": {
          "200": {
            "description": "Site analytics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsSiteReport"
                },
                "example": {
                  "from": "2026-10-05T00:00:00Z",
                  "to": "2026-10-06T00:00:00Z",
                  "interval": "1h",
                  "hosts": [
                    "tv.example.co.il"
                  ],
                  "page_views": 18240,
                  "visitors": 6110,
                  "sessions": 7302,
                  "tile_clicks": 2984,
                  "searches": 412,
                  "series": [
                    {
                      "t": "2026-10-05T19:00:00Z",
                      "views": 1620,
                      "visitors": 702
                    }
                  ],
                  "top_pages": [
                    {
                      "path": "/",
                      "views": 6120,
                      "visitors": 4011
                    }
                  ],
                  "sections": [
                    {
                      "section": "hero",
                      "type": "hero",
                      "path": "/",
                      "views": 5980,
                      "clicks": 811,
                      "ctr": 0.136
                    }
                  ],
                  "search_terms": [
                    {
                      "query": "חדשות",
                      "searches": 41,
                      "zero_results": 0
                    }
                  ],
                  "referrers": [
                    {
                      "host": "www.google.com",
                      "views": 2210
                    }
                  ],
                  "positions": [
                    {
                      "position": 1,
                      "clicks": 1022,
                      "share": 0.36,
                      "ctr": 0.051
                    }
                  ],
                  "section_views": 20110,
                  "position_unknown": 0,
                  "not_found": 37,
                  "not_found_pages": [
                    {
                      "path": "/old-show",
                      "hits": 19,
                      "visitors": 17,
                      "referrer": "www.facebook.com",
                      "internal": 2
                    }
                  ],
                  "not_found_since": "2026-09-12T10:04:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`), or the statistics store is unavailable (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/ads": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Client-side ad figures",
        "description": "**Required scope:** `stats:read`\n\nClient-side ad figures — requests, impressions, fill rate, quartiles, completes, skips, clicks/CTR, errors by VAST code; grouped\n\nScope `stats:read`. From the player's ad events: totals, one row per `group_by` value (default position, ranked\nby ad requests; `limit` 1–200, default 20), errors by VAST code, and a requests / impressions / completes series\nat `interval` (default 1h; 1m ≤ 24 h, 1h ≤ 90 days, 1d ≤ 2 years). `source`: live = real ads (default), test =\ntest-mode demo ads, all. Rates: fill = impressions / requests, completion and skip = per impression, ctr =\nclicks / impressions. Cached 30 s.",
        "operationId": "statsAds",
        "parameters": [
          {
            "name": "group_by",
            "in": "query",
            "description": "Grouping of `groups`",
            "schema": {
              "type": "string",
              "enum": [
                "position",
                "server",
                "creative",
                "channel",
                "asset",
                "country",
                "device"
              ],
              "default": "position"
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "live = real ads (default), test = test-mode demo ads, all",
            "schema": {
              "type": "string",
              "enum": [
                "live",
                "test",
                "all"
              ],
              "default": "live"
            }
          },
          {
            "name": "interval",
            "in": "query",
            "description": "Series bucket size",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "1h",
                "1d"
              ],
              "default": "1h"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Rows in `groups`",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 20
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          }
        ],
        "responses": {
          "200": {
            "description": "Ad figures",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsAds"
                },
                "example": {
                  "from": "2026-10-05T00:00:00Z",
                  "to": "2026-10-06T00:00:00Z",
                  "group_by": "position",
                  "source": "live",
                  "totals": {
                    "requests": 2400,
                    "impressions": 1980,
                    "starts": 1975,
                    "q1": 1890,
                    "mid": 1760,
                    "q3": 1610,
                    "completes": 1502,
                    "skips": 210,
                    "clicks": 31,
                    "errors": 44,
                    "fill_rate": 0.825,
                    "completion_rate": 0.759,
                    "skip_rate": 0.106,
                    "ctr": 0.016
                  },
                  "groups": [
                    {
                      "key": "preroll",
                      "requests": 2400,
                      "impressions": 1980,
                      "starts": 1975,
                      "q1": 1890,
                      "mid": 1760,
                      "q3": 1610,
                      "completes": 1502,
                      "skips": 210,
                      "clicks": 31,
                      "errors": 44,
                      "fill_rate": 0.825,
                      "completion_rate": 0.759,
                      "skip_rate": 0.106,
                      "ctr": 0.016
                    }
                  ],
                  "errors": [
                    {
                      "code": "303",
                      "count": 30
                    },
                    {
                      "code": "402",
                      "count": 14
                    }
                  ],
                  "interval": "1h",
                  "series": [
                    {
                      "t": "2026-10-05T19:00:00Z",
                      "requests": 310,
                      "impressions": 262,
                      "completes": 198
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/ads/decisions": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Per-session server-side ad decision log — JSON, NDJSON or CSV, for reconciliation with the ad server",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. One row per (viewer session, ad break) decision of server-side ad insertion, written when\nthe pod is first stitched: the adapter, the outcome (`filled`, `no_fill`, `error`, `timeout`, `capped` = the\nsession reached the channel's frequency cap), the VAST error code, the decision latency, whether the fallback\ntag answered, the ads returned and stitched (with their ad and creative ids). `session_id` is the player's\nrandom playback session id (`?sid=`), never a person. Impressions themselves are the `ad_impression` events of\n`/v1/stats/ads` (`ad_src` ssai). Newest first; `from`/`to` RFC 3339 (default the last 24 h, at most 31 days);\n`channel` a channel id; `limit` 1–100000 (default 10000; `truncated` says it was reached). Kept 90 days.",
        "operationId": "statsAdDecisions",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "RFC 3339 (default to − 24 h)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "RFC 3339 (default now)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "channel",
            "in": "query",
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000,
              "default": 10000
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "json (default), ndjson or csv (downloads)",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "ndjson",
                "csv"
              ],
              "default": "json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The decisions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "truncated": {
                      "type": "boolean"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdDecision"
                      }
                    }
                  }
                },
                "example": {
                  "from": "2026-10-07T08:00:00Z",
                  "to": "2026-10-08T08:00:00Z",
                  "truncated": false,
                  "items": [
                    {
                      "channel_id": "01a0f3c2-7b1e-7d4a-9c55-2e8f1a6b3d90",
                      "channel": "now14",
                      "break_id": "0199b1f0-2c44-7a10-8e21-6f0b3c9d1e22",
                      "session_id": "k3J9x2",
                      "decided_at": "2026-10-08T07:41:02Z",
                      "adapter": "freewheel",
                      "outcome": "filled",
                      "latency_ms": 212,
                      "used_fallback": false,
                      "ads_returned": 3,
                      "ads_stitched": 2,
                      "stitched_ms": 30000,
                      "ads": [
                        {
                          "ad_id": "fw-1",
                          "creative_id": "c-77",
                          "ad_system": "FreeWheel",
                          "duration_ms": 15000
                        }
                      ]
                    }
                  ]
                }
              },
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0082 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/ads/reconciliation": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Server-side ad decisions by outcome and ads stitched, per channel",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Sums the decision log over `from`/`to` (RFC 3339, default the last 24 h, at most 31 days)\nper channel: decisions, filled, no_fill, errors, timeouts, capped, fallbacks, ads stitched and stitched\nseconds — to set against the ad server's own delivery report and the impressions of `/v1/stats/ads`.",
        "operationId": "statsAdReconciliation",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per channel",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "channels": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdReconciliationRow"
                      }
                    }
                  }
                },
                "example": {
                  "from": "2026-10-07T08:00:00Z",
                  "to": "2026-10-08T08:00:00Z",
                  "channels": [
                    {
                      "channel": "now14",
                      "decisions": 1200,
                      "filled": 1010,
                      "no_fill": 150,
                      "errors": 12,
                      "timeouts": 18,
                      "capped": 10,
                      "fallbacks": 40,
                      "ads_stitched": 2240,
                      "stitched_seconds": 33600
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0082 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/sessions": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Session search for support cases; `vid` only with scope `stats:pii`",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Playback sessions newest first (by start), `limit` 1–500, default 50; no cursor — narrow the\nrange or filters to see older ones. `has_error` = true lists sessions with at least one error event (false: none).\n`q` (≤ 100 characters) matches a session id prefix, a channel slug, an asset or clip id (exact), or a substring of\nthe ISP or page host. The hashed viewer id `vid` is returned — and searchable with `q` — only when the caller also\nholds `stats:pii`. All session filters apply. Cached 30 s.",
        "operationId": "statsSessions",
        "parameters": [
          {
            "name": "has_error",
            "in": "query",
            "description": "true = sessions with errors, false = without",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Free-text search: session id prefix, channel slug, asset or clip id, ISP or page host (substring); the viewer id too with `stats:pii`",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Sessions (1–500)",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsPathway"
          },
          {
            "$ref": "#/components/parameters/StatsLive"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsProgramme"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          },
          {
            "$ref": "#/components/parameters/StatsClip"
          },
          {
            "$ref": "#/components/parameters/StatsPageHost"
          }
        ],
        "responses": {
          "200": {
            "description": "Sessions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StatsSession"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "sid": "019a5c3e-7b4e-7c2e-9a1b-3c5d7e9f1a2b",
                      "start": "2026-10-05T19:02:11Z",
                      "end": "2026-10-05T19:41:52Z",
                      "channel": "main",
                      "live": true,
                      "watched_ms": 2361000,
                      "startup_ms": 840,
                      "rebuffers": 1,
                      "rebuffer_ms": 1200,
                      "errors": 0,
                      "fatal": false,
                      "completed": false,
                      "avg_bitrate_kbps": 3120,
                      "country": "IL",
                      "isp": "Partner Communications",
                      "device": "phone",
                      "os": "iOS",
                      "browser": "Safari",
                      "pathway": "il",
                      "page_host": "www.example.co.il"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/sessions/{sid}": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "One session with its full event timeline",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. The session row plus its player events in time order (at most 5,000), each with the\nnon-empty event fields (startup_ms, level_kbps, level_h, watched_ms, pos_ms, buf_ms, ms, from/to kbps, from/to ms,\nreason, code, fatal, detail, ad_id, ad_pos). `vid` only with `stats:pii`. A session of another tenant, or an id\nlonger than 64 characters, is 404. Cached 30 s.",
        "operationId": "statsSession",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "Session id (as the player sent it)",
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsSessionDetail"
                },
                "example": {
                  "sid": "019a5c3e-7b4e-7c2e-9a1b-3c5d7e9f1a2b",
                  "start": "2026-10-05T19:02:11Z",
                  "end": "2026-10-05T19:41:52Z",
                  "channel": "main",
                  "live": true,
                  "watched_ms": 2361000,
                  "startup_ms": 840,
                  "rebuffers": 1,
                  "rebuffer_ms": 1200,
                  "errors": 0,
                  "fatal": false,
                  "completed": false,
                  "avg_bitrate_kbps": 3120,
                  "country": "IL",
                  "isp": "Partner Communications",
                  "device": "phone",
                  "os": "iOS",
                  "browser": "Safari",
                  "pathway": "il",
                  "page_host": "www.example.co.il",
                  "events": [
                    {
                      "t": "2026-10-05T19:02:11Z",
                      "e": "session_start"
                    },
                    {
                      "t": "2026-10-05T19:02:12Z",
                      "e": "first_frame",
                      "fields": {
                        "startup_ms": 840,
                        "level_kbps": 2500,
                        "level_h": 720
                      }
                    },
                    {
                      "t": "2026-10-05T19:20:40Z",
                      "e": "rebuffer",
                      "fields": {
                        "ms": 1200,
                        "pos_ms": 1108000
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/viewers/{vid}": {
      "delete": {
        "tags": [
          "insight"
        ],
        "summary": "erase one viewer (DSAR) — every statistics row of this tenant carrying the viewer's id",
        "description": "**Required scope:** `stats:pii`\n\nScope `stats:pii`. `vid` is the RAW id the viewer's browser holds (`localStorage.vs_vid` → `id`, a UUID; the\nviewer can read it in the browser, or the player shows it on request). The stored per-tenant, per-year hashes are\nrecomputed and every ClickHouse row of this tenant with one of them is deleted synchronously (player_events,\nsessions, site_events). Rollups keep only aggregate uniq states, from which no id can be read or removed; they\nage out by TTL. The id is then suppressed: insight stores and exports it as \"no id\" (within a minute).\nIdempotent — repeat it on 502. Audited as `viewer.erase` with the hashed id only; the raw id is never logged.",
        "operationId": "deleteStatsViewer",
        "parameters": [
          {
            "name": "vid",
            "in": "path",
            "required": true,
            "description": "The raw viewer id from the browser (a UUID)",
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsViewerErasure"
                },
                "example": {
                  "customer": "tv10poc",
                  "viewer_hash": "9c41d7e0a2b35f8e61c0d94a7b2e3f51",
                  "tables": [
                    {
                      "table": "player_events",
                      "rows": 412,
                      "ms": 180
                    },
                    {
                      "table": "sessions",
                      "rows": 9,
                      "ms": 64
                    },
                    {
                      "table": "site_events",
                      "rows": 0,
                      "ms": 22
                    }
                  ],
                  "rows": 421,
                  "ms": 266,
                  "forms": 7,
                  "suppressed": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The statistics store did not complete the deletion; retry (idempotent)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Viewer ids or statistics are not configured (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:pii"
      }
    },
    "/v1/stats/programmes": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "programme ratings — per EPG programme of one channel the average and peak minute audience…",
        "description": "**Required scope:** `stats:read`\n\nprogramme ratings — per EPG programme of one channel the average and peak minute audience (concurrent viewers), distinct viewers and plays started, plus the same per show\n\nScope `stats:read`. The channel's per-minute concurrent-viewer curve is cut at the EPG programme boundaries; a\nminute belongs to the programme that covers more than 30 s of it, and a minute without a sample counts as 0\nviewers (`minutes_with_data` tells the coverage; an open programme runs until the next one starts, else now).\nShows are the series the EPG matcher attached, else the programme title (`key` = `title:<title>`). Range at most\n31 days and 2,000 programmes (422). `channel` (id or slug) is required (422); a channel of another tenant is 404.\nThe per-minute curve of one programme comes from /v1/stats/timeseries?metric=concurrent&interval=1m&channel=….\nCached 30 s.",
        "operationId": "statsProgrammes",
        "parameters": [
          {
            "name": "channel",
            "in": "query",
            "required": true,
            "description": "Channel id or slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          }
        ],
        "responses": {
          "200": {
            "description": "Programme ratings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsProgrammeReport"
                },
                "example": {
                  "channel": "main",
                  "from": "2026-10-05T00:00:00Z",
                  "to": "2026-10-06T00:00:00Z",
                  "programmes": [
                    {
                      "id": "01995e0a-1c2d-7e3f-8a4b-5c6d7e8f9a0b",
                      "title": "המהדורה המרכזית",
                      "start": "2026-10-05T17:00:00Z",
                      "end": "2026-10-05T18:00:00Z",
                      "series_id": "01990c4b-2d3e-7f40-9b5c-6d7e8f9a0b1c",
                      "series_title": "המהדורה המרכזית",
                      "avg_concurrent": 318.4,
                      "peak_concurrent": 402,
                      "peak_at": "2026-10-05T17:21:00Z",
                      "minutes": 60,
                      "minutes_with_data": 60,
                      "viewers": 1210,
                      "plays": 864,
                      "on_air": false
                    }
                  ],
                  "series": [
                    {
                      "key": "01990c4b-2d3e-7f40-9b5c-6d7e8f9a0b1c",
                      "title": "המהדורה המרכזית",
                      "programmes": 1,
                      "avg_concurrent": 318.4,
                      "peak_concurrent": 402,
                      "viewers": 1210,
                      "plays": 864
                    }
                  ],
                  "data_since": "2026-10-05T00:00:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/qoe": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 2.2: quality of experience",
        "description": "**Required scope:** `stats:read`\n\ninsight-v2 2.2: quality of experience — startup time percentiles, rebuffer ratio, error rate, bitrate switches, average bitrate, watch time per rendition, and the same over time\n\nScope `stats:read`. From the sessions (by session start) and the player heartbeats, so the range must start within\nthe 89-day session retention (422 otherwise). `startup_ms` is null when no play in the range measured a startup\n(players before vs-beacon 1.1.0); `data_since` is null when there are no sessions at all. error_rate = sessions\nwith a fatal error / sessions; rebuffer_ratio = stalled / (watched + stalled); renditions = visible watch time per\nrendition height with its watch-time-weighted declared bitrate (height 0 or kbps 0 = not reported). `interval`\ndefaults to 1m up to 24 h, 1h up to 90 days. Session filters and `programme` apply. Cached 30 s.",
        "operationId": "statsQoE",
        "parameters": [
          {
            "name": "interval",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "1h",
                "1d"
              ]
            },
            "description": "series step; default 1m up to 24 h, 1h up to 90 days"
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsPathway"
          },
          {
            "$ref": "#/components/parameters/StatsLive"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          },
          {
            "$ref": "#/components/parameters/StatsClip"
          },
          {
            "$ref": "#/components/parameters/StatsPageHost"
          },
          {
            "$ref": "#/components/parameters/StatsProgramme"
          }
        ],
        "responses": {
          "200": {
            "description": "QoE report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsQoE"
                },
                "example": {
                  "from": "2026-10-05T00:00:00Z",
                  "to": "2026-10-06T00:00:00Z",
                  "interval": "1h",
                  "sessions": 1310,
                  "plays": 1200,
                  "startup_ms": {
                    "p50": 450,
                    "p75": 690,
                    "p90": 1010,
                    "p95": 1300,
                    "p99": 2650,
                    "measured": 1184
                  },
                  "rebuffer_ratio": 0.002,
                  "rebuffers": 96,
                  "rebuffer_ms": 86400,
                  "watch_time_ms": 43200000,
                  "rebuffers_per_hour": 8,
                  "error_rate": 0.004,
                  "fatal_sessions": 5,
                  "errors": 31,
                  "switches": 2210,
                  "switches_per_play": 1.84,
                  "switches_per_hour": 184.2,
                  "avg_bitrate_kbps": 3400,
                  "renditions": [
                    {
                      "height": 1080,
                      "kbps": 5000,
                      "watch_time_ms": 21600000,
                      "share": 0.5
                    },
                    {
                      "height": 720,
                      "kbps": 2500,
                      "watch_time_ms": 17280000,
                      "share": 0.4
                    }
                  ],
                  "rendition_source": "player_events",
                  "series": [
                    {
                      "t": "2026-10-05T19:00:00Z",
                      "plays": 240,
                      "startup_p50_ms": 430,
                      "startup_p95_ms": 1250,
                      "rebuffer_ratio": 0.0018,
                      "error_rate": 0.004,
                      "switches_per_play": 1.9,
                      "avg_bitrate_kbps": 3450
                    }
                  ],
                  "data_since": "2026-10-05T00:00:41Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/smoothness": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "v3: the 0–100 smoothness score of the range, split by pathway, ISP, ASN, device, site and more, with a…",
        "description": "**Required scope:** `stats:read`\n\nv3: the 0–100 smoothness score of the range, split by pathway, ISP, ASN, device, site and more, with a series\n\nScope `stats:read`. From the player sessions (the same data as /v1/stats/qoe), so the range must start within the\nsession retention (422 otherwise). Per session: a fatal error scores 0; an exit before the first frame (no fatal\nerror) is not scored and is counted in `exits_before_start`; every other session scores\n100 × max(0, 1 − 0.55·min(1, rebuffer_ratio / 0.05) − 0.30·min(1, max(0, startup_s − 1) / 7) − 0.15·min(1,\nswitches_per_hour / 30)). A group's score is the watch-time-weighted mean of its sessions (each counts at least one\nminute); `score` is null when nothing was scored. The formula is returned in `formula`. `group_by` = pathway, isp,\nasn, device, os, browser, engine, page_host, country, channel, asset or live; `limit` 1–200 (default 50) groups by\nwatch time. Session filters and `programme` apply. Cached 30 s.",
        "operationId": "statsSmoothness",
        "parameters": [
          {
            "name": "interval",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "1h",
                "1d"
              ]
            },
            "description": "series step; default 1m up to 24 h, 1h up to 90 days"
          },
          {
            "name": "group_by",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pathway",
                "isp",
                "asn",
                "device",
                "os",
                "browser",
                "engine",
                "page_host",
                "country",
                "channel",
                "asset",
                "live"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsPathway"
          },
          {
            "$ref": "#/components/parameters/StatsLive"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          },
          {
            "$ref": "#/components/parameters/StatsClip"
          },
          {
            "$ref": "#/components/parameters/StatsPageHost"
          },
          {
            "$ref": "#/components/parameters/StatsProgramme"
          }
        ],
        "responses": {
          "200": {
            "description": "Smoothness report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsSmoothness"
                },
                "example": {
                  "from": "2026-09-30T00:00:00Z",
                  "to": "2026-10-07T00:00:00Z",
                  "interval": "1h",
                  "group_by": "isp",
                  "formula": "per session: fatal = 0; …",
                  "total": {
                    "score": 93.6,
                    "sessions": 316,
                    "scored": 246,
                    "exits_before_start": 70,
                    "fatal": 0,
                    "rebuffer_ratio": 0.083,
                    "startup_p50_ms": 519,
                    "switches_per_hour": 7.6,
                    "watch_time_ms": 81000000
                  },
                  "rows": [
                    {
                      "key": "Bezeq International",
                      "score": 96.1,
                      "sessions": 120,
                      "scored": 110,
                      "exits_before_start": 10,
                      "fatal": 0,
                      "rebuffer_ratio": 0.002,
                      "startup_p50_ms": 410,
                      "switches_per_hour": 3.1,
                      "watch_time_ms": 36000000
                    }
                  ],
                  "series": [
                    {
                      "t": "2026-10-06T19:00:00Z",
                      "score": 95.2,
                      "scored": 31
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/cdn": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 2.2: CDN delivery",
        "description": "**Required scope:** `stats:read`\n\ninsight-v2 2.2: CDN delivery — requests, bytes, Gbps, hit ratio (requests and bytes), origin bytes and average request time in total, per edge, per cache status and per status class, and over time\n\nScope `stats:read`; available to CDN-only tenants. From the edge access logs (1-minute rollup, 1-hour beyond 89\ndays), the origin head's own rows excluded as in /v1/stats/traffic. The edge logs carry no content id per row:\nchannel / asset / clip / programme filters do not apply and `content_filter_ignored` says so; only `country` does.\n`data_since` is null when the tenant delivered nothing in the range. Cache status '' or '-' is reported as NONE\n(not cacheable, e.g. beacons and errors). `interval` defaults by range as in timeseries. Cached 30 s.",
        "operationId": "statsCDN",
        "parameters": [
          {
            "name": "interval",
            "in": "query",
            "description": "Series bucket size; default 1m up to 24 h, 1h up to 90 days, else 1d",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "1h",
                "1d"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          }
        ],
        "responses": {
          "200": {
            "description": "CDN report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsCDN"
                },
                "example": {
                  "from": "2026-10-05T00:00:00Z",
                  "to": "2026-10-06T00:00:00Z",
                  "interval": "1h",
                  "totals": {
                    "requests": 25730753,
                    "bytes": 13888889889888,
                    "hit_ratio_requests": 0.98,
                    "hit_ratio_bytes": 0.99,
                    "origin_bytes": 141000000000,
                    "avg_request_ms": 12.4,
                    "gbps": 1.286
                  },
                  "edges": [
                    {
                      "site": "med1",
                      "edge": "r6625-med1",
                      "share": 0.71,
                      "requests": 18420311,
                      "bytes": 9876543210987,
                      "hit_ratio_requests": 0.982,
                      "hit_ratio_bytes": 0.991,
                      "origin_bytes": 0,
                      "avg_request_ms": 11.9,
                      "gbps": 0.914
                    }
                  ],
                  "cache_status": [
                    {
                      "key": "HIT",
                      "requests": 25010211,
                      "bytes": 13750000000000,
                      "share": 0.972
                    },
                    {
                      "key": "MISS",
                      "requests": 520311,
                      "bytes": 138000000000,
                      "share": 0.02
                    }
                  ],
                  "status_class": [
                    {
                      "key": "2xx",
                      "requests": 25600112,
                      "bytes": 13888000000000,
                      "share": 0.995
                    }
                  ],
                  "series": [
                    {
                      "t": "2026-10-05T19:00:00Z",
                      "gbps": 2.31,
                      "hit_ratio_requests": 0.983,
                      "hit_ratio_bytes": 0.991,
                      "requests": 1902231
                    }
                  ],
                  "content_filter_ignored": false,
                  "data_since": "2026-10-05T00:00:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/completion": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 1.2: completion quartiles",
        "description": "**Required scope:** `stats:read`\n\ninsight-v2 1.2: completion quartiles — on-demand plays (VOD, clips, catch-up) that reached 25 / 50 / 75 % of the duration and that completed, in total or per asset, clip or programme\n\nScope `stats:read`. A play counts when it reached a first frame, is not live and its duration is known; it\nreached a quartile when its furthest position is at or past that share of the duration (100 % = ended or ≥ 95 %).\nWith `channel`, the programmes of that channel are meant (catch-up, start-over and programme VODs). Within the\n89-day session retention the exact range is used (`source` = sessions); older ranges read the daily rollup\n(whole days, content filters only; `source` = rollup_1d). `group_by=programme` rows carry `title`, `start` and\n`channel`; `group_by=asset` rows carry the asset `title`. `limit` 1–500, default 100. Cached 30 s.",
        "operationId": "statsCompletion",
        "parameters": [
          {
            "name": "group_by",
            "in": "query",
            "description": "Rows per asset, clip or programme (omitted = total only)",
            "schema": {
              "type": "string",
              "enum": [
                "asset",
                "clip",
                "programme"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Rows (1–500)",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          },
          {
            "$ref": "#/components/parameters/StatsClip"
          },
          {
            "$ref": "#/components/parameters/StatsProgramme"
          }
        ],
        "responses": {
          "200": {
            "description": "Completion quartiles",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsCompletion"
                },
                "example": {
                  "from": "2026-09-29T00:00:00Z",
                  "to": "2026-10-06T00:00:00Z",
                  "group_by": "asset",
                  "source": "sessions",
                  "total": {
                    "plays": 2140,
                    "q25": 1610,
                    "q50": 1180,
                    "q75": 902,
                    "q100": 655,
                    "q25_rate": 0.752,
                    "q50_rate": 0.551,
                    "q75_rate": 0.421,
                    "q100_rate": 0.306,
                    "watch_time_ms": 91000000
                  },
                  "rows": [
                    {
                      "key": "01928f10-6a2d-7e41-8c3b-5d9e1f2a3b4c",
                      "title": "המהדורה המרכזית 05.10",
                      "plays": 384,
                      "q25": 301,
                      "q50": 240,
                      "q75": 199,
                      "q100": 158,
                      "q25_rate": 0.784,
                      "q50_rate": 0.625,
                      "q75_rate": 0.518,
                      "q100_rate": 0.411,
                      "watch_time_ms": 22100000
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/StatsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/export": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 2.3: raw export — one report streamed as CSV (UTF-8 with BOM) or NDJSON, at most 1,000,000 rows",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Reports: `sessions` (one row per playback session), `player_events` (one row per beacon event),\n`edge_requests` (the tenant's edge access-log lines; filter: country only), `site_events` (Sites page views,\nclicks, searches, 404s; filters: country, device, page_host), `programmes` (per EPG programme: plays, viewers,\nwatch time, completion quartiles, rebuffer ratio). A filter the report cannot apply is refused (422). Range at most\n31 days. The rows are counted first: `X-Export-Total-Rows` and `X-Export-Truncated` are headers; the trailers\n`X-Export-Rows` / `X-Export-Status` (complete | failed) say what was written, and a failure after the first byte\naborts the connection rather than ending a short file cleanly. Viewer-level columns (`vid`, hashed IP, viewer\nattributes, full page URL, User-Agent, referer, query strings, session ids on edge lines) only with `stats:pii`.\nText cells starting with = + - @ get a leading apostrophe. Per tenant: one export running at a time (409), 10\nexports per hour together with prepared files (429, `Retry-After: 600`). Audited as `stats.export`.",
        "operationId": "statsExport",
        "parameters": [
          {
            "name": "report",
            "in": "query",
            "required": true,
            "description": "The report",
            "schema": {
              "type": "string",
              "enum": [
                "sessions",
                "player_events",
                "edge_requests",
                "site_events",
                "programmes",
                "qoe_mux"
              ]
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "csv (UTF-8 with BOM) or ndjson",
            "schema": {
              "type": "string",
              "enum": [
                "csv",
                "ndjson"
              ],
              "default": "csv"
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          },
          {
            "$ref": "#/components/parameters/StatsCountry"
          },
          {
            "$ref": "#/components/parameters/StatsDevice"
          },
          {
            "$ref": "#/components/parameters/StatsPathway"
          },
          {
            "$ref": "#/components/parameters/StatsLive"
          },
          {
            "$ref": "#/components/parameters/StatsChannel"
          },
          {
            "$ref": "#/components/parameters/StatsAsset"
          },
          {
            "$ref": "#/components/parameters/StatsClip"
          },
          {
            "$ref": "#/components/parameters/StatsPageHost"
          },
          {
            "$ref": "#/components/parameters/StatsProgramme"
          }
        ],
        "responses": {
          "200": {
            "description": "The report, streamed as an attachment `viewstream-<tenant>-<report>-<from>-<to>.<format>`",
            "headers": {
              "X-Export-Id": {
                "description": "Id of the export (audit log)",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "X-Export-Total-Rows": {
                "description": "Rows of the report in the range before the cap",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Export-Truncated": {
                "description": "true when the file stops at X-Export-Max-Rows",
                "schema": {
                  "type": "boolean"
                }
              },
              "X-Export-Max-Rows": {
                "schema": {
                  "type": "integer",
                  "example": 1000000
                }
              },
              "X-Export-Rows": {
                "description": "Trailer: rows written",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Export-Status": {
                "description": "Trailer: complete or failed",
                "schema": {
                  "type": "string",
                  "enum": [
                    "complete",
                    "failed"
                  ]
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                },
                "example": "﻿sid,start,end,channel,asset,clip,live,watched_ms,startup_ms,country,device\n019a5c3e-7b4e-7c2e-9a1b-3c5d7e9f1a2b,2026-10-05T19:02:11Z,2026-10-05T19:41:52Z,main,,,true,2361000,840,IL,phone\n"
              },
              "application/x-ndjson": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "description": "Statistics store or exports not configured / unavailable",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/support/requests": {
      "get": {
        "tags": [
          "support"
        ],
        "summary": "The tenant's support requests, newest first (cursor)",
        "description": "**Required scope:** `support:read`\n\nScope `support:read` (every Studio role). Newest first; `limit` 1–200 (default 50); pass `next_cursor` as\n`cursor` for the next page. Filters `status`, `category`, `priority`. `counts` = the tenant's requests per status.",
        "operationId": "listSupportRequests",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "in_progress",
                "waiting_customer",
                "resolved",
                "closed"
              ]
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "question",
                "bug",
                "outage",
                "billing",
                "feature_request",
                "other"
              ]
            }
          },
          {
            "name": "priority",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "normal",
                "high",
                "urgent"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The next_cursor of the previous page"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Support requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportRequestPage"
                },
                "example": {
                  "items": [
                    {
                      "id": "019a6c10-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
                      "tenant_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "tenant_slug": "now14poc",
                      "tenant_name": "Channel 14 PoC",
                      "created_by_user": "019286a2-4c33-7c4b-9a1b-3c5d7e9f1a2e",
                      "created_by_key": null,
                      "created_by": "user:dana@example.co.il",
                      "contact_email": "dana@example.co.il",
                      "contact_name": "Dana Levi",
                      "lang": "he",
                      "category": "bug",
                      "priority": "high",
                      "subject": "הנגן לא עולה בערוץ הראשי",
                      "message": "מאז 09:00 הנגן מציג שגיאה בדף הערוץ.",
                      "context": {
                        "page_path": "/t/now14poc/live/019286a2-7a10-7c4b-9a1b-3c5d7e9f1a40",
                        "studio_version": "f16a21d2",
                        "time_zone": "Asia/Jerusalem",
                        "locale": "he",
                        "request_ids": [
                          "c0a8012e-5f3a"
                        ],
                        "channel_id": "019286a2-7a10-7c4b-9a1b-3c5d7e9f1a40"
                      },
                      "status": "open",
                      "assignee": null,
                      "created_at": "2026-10-06T09:12:00Z",
                      "updated_at": "2026-10-06T09:12:00Z",
                      "last_reply_at": null,
                      "message_count": 0
                    }
                  ],
                  "next_cursor": null,
                  "counts": {
                    "open": 1,
                    "in_progress": 0,
                    "waiting_customer": 0,
                    "resolved": 0,
                    "closed": 0
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "support:read"
      },
      "post": {
        "tags": [
          "support"
        ],
        "summary": "Open a support request (mailed to Interhost support)",
        "description": "**Required scope:** `support:write`\n\nScope `support:write` (every Studio role has it; an API key needs it explicitly). Also allowed while the tenant\nis suspended. `category` question | bug | outage | billing | feature_request | other; `priority` normal (default)\n| high | urgent — urgent only with category outage (422 otherwise); `subject` 1–200 characters (line breaks become\nspaces); `message` 1–10000 characters. `contact_email` defaults to the signed-in user's address (required with an\nAPI key; validated); `contact_name` ≤ 200 defaults to the user's name. `lang` he | en is the language of the\nmails to the requester (default: the user's language, else Hebrew). `context` is what Studio attaches: page path\n(query strings and fragments are dropped), Studio version, browser, time zone, UI language, up to 20 recent\nrequest ids, up to 10 recent errors (texts bounded, anything that looks like a key or token is redacted), the\nchannel or asset on screen — never tokens or cookies. Body ≤ 96 KiB, unknown fields refused.\n\nAt most 10 new requests per tenant per hour (429, `Retry-After`). The request is mailed to the support inbox\n(SUPPORT_EMAIL, Reply-To = the contact); an urgent outage also raises the operator alert SupportUrgentOutage.\nAudited as `support.request_create` (category and priority only).",
        "operationId": "createSupportRequest",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SupportRequestInput"
              },
              "example": {
                "category": "bug",
                "priority": "high",
                "subject": "הנגן לא עולה בערוץ הראשי",
                "message": "מאז 09:00 הנגן מציג שגיאה בדף הערוץ.",
                "lang": "he",
                "context": {
                  "page_path": "/t/now14poc/live/019286a2-7a10-7c4b-9a1b-3c5d7e9f1a40",
                  "studio_version": "f16a21d2",
                  "time_zone": "Asia/Jerusalem",
                  "locale": "he",
                  "request_ids": [
                    "c0a8012e-5f3a"
                  ],
                  "channel_id": "019286a2-7a10-7c4b-9a1b-3c5d7e9f1a40"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new request (no messages yet)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportRequestDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "support:write"
      }
    },
    "/v1/support/requests/{id}": {
      "get": {
        "tags": [
          "support"
        ],
        "summary": "One support request with its conversation",
        "description": "**Required scope:** `support:read`\n\nScope `support:read`. The request and its messages, oldest first. A request of another tenant is 404.",
        "operationId": "getSupportRequest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Support request id"
          }
        ],
        "responses": {
          "200": {
            "description": "Request with messages",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportRequestDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "support:read"
      }
    },
    "/v1/support/requests/{id}/messages": {
      "post": {
        "tags": [
          "support"
        ],
        "summary": "Reply to a support request (reopens it when resolved, closed or waiting for you)",
        "description": "**Required scope:** `support:write`\n\nScope `support:write`. `body` 1–10000 characters. A request in status resolved, closed or waiting_customer goes\nback to `open`. At most 30 customer messages per request per day (429). The reply is mailed to the support inbox.\nAudited as `support.reply` (and `support.status` when the status changed).",
        "operationId": "replySupportRequest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Support request id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "additionalProperties": false,
                "properties": {
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 10000
                  }
                }
              },
              "example": {
                "body": "עדיין קורה, מצרפת צילום מסך במייל."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The request with its conversation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportRequestDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "support:write"
      }
    },
    "/v1/support/requests/{id}/close": {
      "post": {
        "tags": [
          "support"
        ],
        "summary": "Close a support request",
        "description": "**Required scope:** `support:write`\n\nScope `support:write`. Sets status `closed` (idempotent). Replying reopens it. Audited as `support.status`.",
        "operationId": "closeSupportRequest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Support request id"
          }
        ],
        "responses": {
          "200": {
            "description": "The closed request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportRequestDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "support:write"
      }
    },
    "/v1/stats/exports": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 2.3: the tenant's prepared export files (last 30 days, newest first) with a download link signed…",
        "description": "**Required scope:** `stats:read`\n\ninsight-v2 2.3: the tenant's prepared export files (last 30 days, newest first) with a download link signed for 15 minutes\n\nScope `stats:read`. Prepared files (not streamed exports) created in the last 30 days, newest first; `limit` 1–100,\ndefault 50, no cursor. `download_url` is set for ready files the caller may download (a file made with `stats:pii`\ncolumns needs `stats:pii`); it needs no session and works for 15 minutes — list again for a fresh one. Files are\ndeleted 7 days after they are ready (`expires_at`; status then `expired`). `reports` lists the report names and\n`limits` the export limits.",
        "operationId": "listStatsExports",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Files (1–100)",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Prepared exports",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "reports",
                    "limits"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StatsExport"
                      }
                    },
                    "reports": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "limits": {
                      "type": "object",
                      "properties": {
                        "max_rows": {
                          "type": "integer"
                        },
                        "max_range_days": {
                          "type": "integer"
                        },
                        "per_hour": {
                          "type": "integer"
                        },
                        "queued": {
                          "type": "integer"
                        },
                        "keep_days": {
                          "type": "integer"
                        },
                        "link_minutes": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "019a6b21-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
                      "delivery": "file",
                      "report": "sessions",
                      "format": "csv",
                      "from": "2026-10-04T00:00:00Z",
                      "to": "2026-10-05T00:00:00Z",
                      "filters": {
                        "channel": "main"
                      },
                      "pii": false,
                      "status": "ready",
                      "rows": 18342,
                      "truncated": false,
                      "bytes": 1203344,
                      "error": null,
                      "email": false,
                      "created_by": null,
                      "created_at": "2026-10-05T07:12:03Z",
                      "started_at": "2026-10-05T07:12:05Z",
                      "finished_at": "2026-10-05T07:12:31Z",
                      "expires_at": "2026-10-12T07:12:31Z",
                      "file_name": "viewstream-tv10poc-sessions-20261004T0000Z-20261005T0000Z.csv.gz",
                      "download_url": "https://api.viewstream.co.il/exports/stats/019a6b21-4c5d-7e6f-8a9b-0c1d2e3f4a5b?exp=1791263451&sig=3f0c…",
                      "download_expires_at": "2026-10-06T08:30:51Z"
                    }
                  ],
                  "reports": [
                    "edge_requests",
                    "player_events",
                    "programmes",
                    "sessions",
                    "site_events"
                  ],
                  "limits": {
                    "max_rows": 1000000,
                    "max_range_days": 31,
                    "per_hour": 10,
                    "queued": 3,
                    "keep_days": 7,
                    "link_minutes": 15
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "description": "Statistics exports not configured (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 2.3: prepare an export file — written gzip-compressed to object storage in the background (202)",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Same reports, filters, columns and limits as GET /v1/stats/export (the body is validated\nexactly like that query string). Answers 202 with the export in status `queued`; a background worker writes\n`<tenant>/stats/<id>.<csv|ndjson>.gz` to the `exports` bucket — poll GET /v1/stats/exports/{id} until `ready` or\n`failed`. With `email: true` (signed-in users only; 422 for an API key) the requester gets a mail when it is ready\nor failed, linking to Studio (no bearer link travels by mail). Per tenant: one export running (409), 10 per hour\nand 3 files waiting (429, `Retry-After: 600`). Audited as `stats.export_create` and `stats.export_ready|failed`.",
        "operationId": "createStatsExport",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "report"
                ],
                "additionalProperties": false,
                "properties": {
                  "report": {
                    "type": "string",
                    "enum": [
                      "sessions",
                      "player_events",
                      "edge_requests",
                      "site_events",
                      "programmes",
                      "qoe_mux"
                    ]
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "csv",
                      "ndjson"
                    ],
                    "default": "csv"
                  },
                  "from": {
                    "type": "string",
                    "description": "RFC 3339 or epoch ms; default to − 24 h"
                  },
                  "to": {
                    "type": "string",
                    "description": "RFC 3339 or epoch ms; default now"
                  },
                  "country": {
                    "type": "string",
                    "description": "ISO 3166 alpha-2"
                  },
                  "device": {
                    "type": "string",
                    "enum": [
                      "phone",
                      "tablet",
                      "desktop",
                      "tv",
                      "bot",
                      "other"
                    ]
                  },
                  "pathway": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string",
                    "description": "channel slug or id"
                  },
                  "asset": {
                    "type": "string"
                  },
                  "clip": {
                    "type": "string"
                  },
                  "page_host": {
                    "type": "string"
                  },
                  "programme": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "live": {
                    "type": "boolean"
                  },
                  "email": {
                    "type": "boolean",
                    "default": false,
                    "description": "Mail the signed-in requester when the file is ready or failed"
                  }
                }
              },
              "example": {
                "report": "sessions",
                "format": "csv",
                "from": "2026-10-04T00:00:00Z",
                "to": "2026-10-05T00:00:00Z",
                "channel": "main",
                "email": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsExport"
                },
                "example": {
                  "id": "019a6b21-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
                  "delivery": "file",
                  "report": "sessions",
                  "format": "csv",
                  "from": "2026-10-04T00:00:00Z",
                  "to": "2026-10-05T00:00:00Z",
                  "filters": {
                    "channel": "main"
                  },
                  "pii": false,
                  "status": "queued",
                  "rows": null,
                  "truncated": false,
                  "bytes": null,
                  "error": null,
                  "email": true,
                  "created_by": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "created_at": "2026-10-05T07:12:03Z",
                  "started_at": null,
                  "finished_at": null,
                  "expires_at": null,
                  "file_name": "viewstream-tv10poc-sessions-20261004T0000Z-20261005T0000Z.csv.gz",
                  "download_url": null,
                  "download_expires_at": null
                }
              }
            }
          },
          "400": {
            "description": "Body is not valid JSON, has unknown fields or is over 8 KB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "description": "Statistics exports or object storage not configured (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/exports/{id}": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 2.3: one prepared export (poll until `ready`; carries a fresh 15-minute download link)",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. One prepared file of the tenant; streamed exports and other tenants' files are 404. While\n`queued` / `running` poll this route; when `ready` it carries `download_url` (signed, 15 minutes, only when the\ncaller may download it — a `pii` file needs `stats:pii`). `failed` carries `error`.",
        "operationId": "getStatsExport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Export id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The export",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsExport"
                },
                "example": {
                  "id": "019a6b21-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
                  "delivery": "file",
                  "report": "sessions",
                  "format": "csv",
                  "from": "2026-10-04T00:00:00Z",
                  "to": "2026-10-05T00:00:00Z",
                  "filters": {
                    "channel": "main"
                  },
                  "pii": false,
                  "status": "ready",
                  "rows": 18342,
                  "truncated": false,
                  "bytes": 1203344,
                  "error": null,
                  "email": true,
                  "created_by": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "created_at": "2026-10-05T07:12:03Z",
                  "started_at": "2026-10-05T07:12:05Z",
                  "finished_at": "2026-10-05T07:12:31Z",
                  "expires_at": "2026-10-12T07:12:31Z",
                  "file_name": "viewstream-tv10poc-sessions-20261004T0000Z-20261005T0000Z.csv.gz",
                  "download_url": "https://api.viewstream.co.il/exports/stats/019a6b21-4c5d-7e6f-8a9b-0c1d2e3f4a5b?exp=1791263451&sig=3f0c…",
                  "download_expires_at": "2026-10-06T08:30:51Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Statistics exports not configured (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/exports/{id}/download": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 2.3: download a ready export file (gzip) with the caller's credentials",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`; a file made with viewer-level columns (`pii: true`) also needs `stats:pii` (403\ninsufficient_scope). Streams the gzip file as an attachment `<file_name>`. 409 while the file is not `ready`;\n404 when it is unknown or no longer in storage. Audited as `stats.export_download`.",
        "operationId": "downloadStatsExport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Export id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The gzip file (attachment)",
            "content": {
              "application/gzip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Statistics exports not configured (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/report/export": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Scheduled reports: \"export now\" — the statistics report as a ZIP (report.html + report.pdf + CSV files)",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. The report the scheduled mail carries: overview vs the previous period, top channels / videos\n/ clips, programme ratings and completion quartiles, QoE and Sites. `cadence=daily` = yesterday (Israel time),\n`weekly` = the last full Sunday–Saturday week; or a custom `from`/`to` of at most 31 days; nothing = daily. A\nsection whose source is missing says \"no data yet\"; one that could not be read says \"unavailable\". CSVs are UTF-8\nwith a BOM. report.pdf is the HTML printed by the internal renderer (A4, Hebrew RTL); when the renderer is off,\nfails or passes its time limit the ZIP goes without it and `X-Report-PDF` says why.",
        "operationId": "exportStatsReport",
        "parameters": [
          {
            "name": "cadence",
            "in": "query",
            "description": "daily = yesterday, weekly = the last full week; ignored with from/to",
            "schema": {
              "type": "string",
              "enum": [
                "daily",
                "weekly"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "description": "Report language",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ],
              "default": "he"
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          }
        ],
        "responses": {
          "200": {
            "description": "ZIP archive (attachment `<report base name>.zip`)",
            "headers": {
              "X-Report-PDF": {
                "description": "Whether report.pdf is in the ZIP",
                "schema": {
                  "type": "string",
                  "enum": [
                    "included",
                    "disabled",
                    "timeout",
                    "failed"
                  ]
                }
              }
            },
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/stats/report/preview": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Scheduled reports: the report email body as HTML (Studio shows it in a sandboxed frame)",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. The same report and period rules as /v1/stats/report/export, rendered as the mail's HTML body\n(without an unsubscribe link). Served with a strict CSP (`default-src 'none'`; framable only by the Studio\norigins) and `X-Content-Type-Options: nosniff`.",
        "operationId": "previewStatsReport",
        "parameters": [
          {
            "name": "cadence",
            "in": "query",
            "description": "daily = yesterday, weekly = the last full week; ignored with from/to",
            "schema": {
              "type": "string",
              "enum": [
                "daily",
                "weekly"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "description": "Report language",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ],
              "default": "he"
            }
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          }
        ],
        "responses": {
          "200": {
            "description": "HTML email body",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/report-schedules": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Scheduled statistics reports of the tenant (daily 08:00 / weekly Sunday 08:00, Israel time) with recipients…",
        "description": "**Required scope:** `stats:read`\n\nScheduled statistics reports of the tenant (daily 08:00 / weekly Sunday 08:00, Israel time) with recipients and the last-sent status\n\nScope `stats:read`. Every schedule of the tenant with its recipients (status pending / active / unsubscribed), the\nnext run and the last send. `can_manage` says whether the caller may change schedules (team:manage or\nnotifications:manage); only then is `members` (active team members to pick from) included. No pagination (at\nmost 20 schedules). Confirmation and unsubscribe tokens never appear.",
        "operationId": "listReportSchedules",
        "responses": {
          "200": {
            "description": "Schedules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "can_manage"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReportSchedule"
                      }
                    },
                    "can_manage": {
                      "type": "boolean"
                    },
                    "timezone": {
                      "type": "string",
                      "example": "Asia/Jerusalem"
                    },
                    "send_hour": {
                      "type": "integer",
                      "example": 8
                    },
                    "limits": {
                      "type": "object",
                      "properties": {
                        "schedules": {
                          "type": "integer"
                        },
                        "recipients": {
                          "type": "integer"
                        }
                      }
                    },
                    "members": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "email": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "019a2f10-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
                      "name": "דוח יומי להנהלה",
                      "cadence": "daily",
                      "lang": "he",
                      "paused": false,
                      "attach_pdf": true,
                      "next_run_at": "2026-10-07T05:00:00Z",
                      "last_run_at": "2026-10-06T05:00:02Z",
                      "last_status": "sent",
                      "last_error": null,
                      "last_trigger": "scheduled",
                      "created_by": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "created_at": "2026-09-20T09:14:00Z",
                      "updated_at": "2026-09-20T09:14:00Z",
                      "recipients": [
                        {
                          "id": "019a2f10-3b4d-7a11-9c22-3d4e5f6a7b8c",
                          "email": "editor@example.co.il",
                          "user_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                          "kind": "member",
                          "status": "active",
                          "confirm_sent_at": null,
                          "confirmed_at": null,
                          "unsubscribed_at": null,
                          "created_at": "2026-09-20T09:14:00Z"
                        }
                      ]
                    }
                  ],
                  "can_manage": true,
                  "timezone": "Asia/Jerusalem",
                  "send_hour": 8,
                  "limits": {
                    "schedules": 20,
                    "recipients": 25
                  },
                  "members": [
                    {
                      "user_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "email": "editor@example.co.il",
                      "name": "Dana Editor",
                      "role": "admin"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "insight"
        ],
        "summary": "Create a scheduled report",
        "description": "**Required scope:** `team:manage` or `notifications:manage`\n\nCreate a scheduled report. Team members receive it at once; an explicit address first gets a confirmation mail (double opt-in, link valid 7 days) and receives nothing until it confirms\n\nScope team:manage or notifications:manage. `name` 1–120 characters; `cadence` daily (default; 08:00 Israel time)\nor weekly (Sunday 08:00); `lang` he (default) or en; `attach_pdf` default true. At most 20 schedules per tenant\nand 25 recipients per schedule (422). Every recipient is validated before anything is written: `members` must be\nactive members, `emails` bare addresses. An address that belongs to an active member is added as that member (no\nconfirmation); any other address gets a confirmation mail (at most 30 per tenant per 24 h — 429). Removed or\ndisabled members stop receiving the report. Audited as `report_schedule.create`.",
        "operationId": "createReportSchedule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportScheduleInput"
              },
              "example": {
                "name": "דוח יומי להנהלה",
                "cadence": "daily",
                "lang": "he",
                "members": [
                  "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b"
                ],
                "emails": [
                  "ceo@example.co.il"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportSchedule"
                },
                "example": {
                  "id": "019a2f10-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
                  "name": "דוח יומי להנהלה",
                  "cadence": "daily",
                  "lang": "he",
                  "paused": false,
                  "attach_pdf": true,
                  "next_run_at": "2026-10-07T05:00:00Z",
                  "last_run_at": null,
                  "last_status": null,
                  "last_error": null,
                  "last_trigger": null,
                  "created_by": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "created_at": "2026-10-06T09:14:00Z",
                  "updated_at": "2026-10-06T09:14:00Z",
                  "recipients": [
                    {
                      "id": "019a2f10-3b4d-7a11-9c22-3d4e5f6a7b8c",
                      "email": "editor@example.co.il",
                      "user_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                      "kind": "member",
                      "status": "active",
                      "confirm_sent_at": null,
                      "confirmed_at": null,
                      "unsubscribed_at": null,
                      "created_at": "2026-10-06T09:14:00Z"
                    },
                    {
                      "id": "019a2f10-3b4d-7b22-8d33-4e5f6a7b8c9d",
                      "email": "ceo@example.co.il",
                      "user_id": null,
                      "kind": "external",
                      "status": "pending",
                      "confirm_sent_at": "2026-10-06T09:14:00Z",
                      "confirmed_at": null,
                      "unsubscribed_at": null,
                      "created_at": "2026-10-06T09:14:00Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ReportBadBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "team:manage or notifications:manage"
      }
    },
    "/v1/report-schedules/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "patch": {
        "tags": [
          "insight"
        ],
        "summary": "Change name, cadence, language, the PDF attachment or pause/resume…",
        "description": "**Required scope:** `team:manage` or `notifications:manage`\n\nChange name, cadence, language, the PDF attachment or pause/resume (a resumed schedule never sends the runs it missed)\n\nScope team:manage or notifications:manage. Only the fields sent change; the same rules as on create apply.\nRecipients cannot be changed here (`members` / `emails` → 422; use …/recipients). Changing the cadence or\nresuming a paused schedule recomputes `next_run_at` from now, so missed runs are never sent. Audited as\n`report_schedule.update`.",
        "operationId": "patchReportSchedule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "cadence": {
                    "type": "string",
                    "enum": [
                      "daily",
                      "weekly"
                    ]
                  },
                  "lang": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ]
                  },
                  "paused": {
                    "type": "boolean"
                  },
                  "attach_pdf": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "paused": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportSchedule"
                },
                "example": {
                  "id": "019a2f10-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
                  "name": "דוח יומי להנהלה",
                  "cadence": "daily",
                  "lang": "he",
                  "paused": true,
                  "attach_pdf": true,
                  "next_run_at": "2026-10-07T05:00:00Z",
                  "last_run_at": "2026-10-06T05:00:02Z",
                  "last_status": "sent",
                  "last_error": null,
                  "last_trigger": "scheduled",
                  "created_by": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "created_at": "2026-09-20T09:14:00Z",
                  "updated_at": "2026-10-06T09:20:00Z",
                  "recipients": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ReportBadBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "team:manage or notifications:manage"
      },
      "delete": {
        "tags": [
          "insight"
        ],
        "summary": "Delete a scheduled report (its recipients and send log go with it)",
        "description": "**Required scope:** `team:manage` or `notifications:manage`\n\nScope team:manage or notifications:manage. Permanent; recipients get no further mails. Audited as `report_schedule.delete`.",
        "operationId": "deleteReportSchedule",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "team:manage or notifications:manage"
      }
    },
    "/v1/report-schedules/{id}/send": {
      "post": {
        "tags": [
          "insight"
        ],
        "summary": "\"Send now\": mail the report of the last completed period (yesterday / last full week) to every deliverable…",
        "description": "**Required scope:** `team:manage` or `notifications:manage`\n\n\"Send now\": mail the report of the last completed period (yesterday / last full week) to every deliverable recipient, marked as a preview (10 per tenant per hour)\n\nScope team:manage or notifications:manage. Builds and mails the report synchronously (up to 3 minutes) to the\nactive recipients (pending and unsubscribed addresses are skipped); paused schedules can be sent too. The answer\nis the recorded send: `sent`, `partial` (some recipients failed), `failed`, or `no_recipients`; `error` lists the\nfailures with masked addresses (also a PDF that could not be attached). At most 10 \"send now\" runs per tenant per\nhour (429). Audited as `report_schedule.send_now`.",
        "operationId": "sendReportNow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Schedule id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The send",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportSend"
                },
                "example": {
                  "id": "019a7c01-5d6e-7f80-9a1b-2c3d4e5f6a7b",
                  "schedule_id": "019a2f10-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
                  "trigger": "manual",
                  "period_from": "2026-10-04T21:00:00Z",
                  "period_to": "2026-10-05T21:00:00Z",
                  "status": "sent",
                  "recipients": 2,
                  "failed": 0,
                  "error": null,
                  "created_at": "2026-10-06T09:21:40Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "team:manage or notifications:manage"
      }
    },
    "/v1/report-schedules/{id}/sends": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "The latest sends of a scheduled report (newest first)",
        "description": "**Required scope:** `stats:read`\n\nScope `stats:read`. Scheduled and manual sends of the schedule, newest first; `limit` 1–100, default 20, no cursor.",
        "operationId": "listReportSends",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Schedule id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Sends (1–100)",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sends",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReportSend"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "019a7c01-5d6e-7f80-9a1b-2c3d4e5f6a7b",
                      "schedule_id": "019a2f10-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
                      "trigger": "scheduled",
                      "period_from": "2026-10-04T21:00:00Z",
                      "period_to": "2026-10-05T21:00:00Z",
                      "status": "partial",
                      "recipients": 2,
                      "failed": 1,
                      "error": "c***@example.co.il: 550 mailbox unavailable",
                      "created_at": "2026-10-06T05:00:02Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/report-schedules/{id}/recipients": {
      "post": {
        "tags": [
          "insight"
        ],
        "summary": "Add a recipient: `user_id` of an active member, or an `email`…",
        "description": "**Required scope:** `team:manage` or `notifications:manage`\n\nAdd a recipient: `user_id` of an active member, or an `email` (a member's address is added as the member; any other address gets a confirmation mail)\n\nScope team:manage or notifications:manage. Give exactly one of `user_id` (an active member) or `email` (a bare\naddress). A member is active at once; any other address is `pending` until it confirms via the mailed link (valid\n7 days; at most 30 confirmation mails per tenant per 24 h — 429). An address already on the schedule is 409; a\nschedule with 25 recipients is 422. Answers the whole schedule. Audited as `report_schedule.recipient_add`.",
        "operationId": "addReportRecipient",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Schedule id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "user_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "An active member"
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "A bare address (no display name), at most 254 characters"
                  }
                }
              },
              "example": {
                "email": "ceo@example.co.il"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The schedule with its recipients",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportSchedule"
                },
                "example": {
                  "id": "019a2f10-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
                  "name": "דוח יומי להנהלה",
                  "cadence": "daily",
                  "lang": "he",
                  "paused": false,
                  "attach_pdf": true,
                  "next_run_at": "2026-10-07T05:00:00Z",
                  "last_run_at": null,
                  "last_status": null,
                  "last_error": null,
                  "last_trigger": null,
                  "created_by": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                  "created_at": "2026-10-06T09:14:00Z",
                  "updated_at": "2026-10-06T09:14:00Z",
                  "recipients": [
                    {
                      "id": "019a2f10-3b4d-7b22-8d33-4e5f6a7b8c9d",
                      "email": "ceo@example.co.il",
                      "user_id": null,
                      "kind": "external",
                      "status": "pending",
                      "confirm_sent_at": "2026-10-06T09:30:00Z",
                      "confirmed_at": null,
                      "unsubscribed_at": null,
                      "created_at": "2026-10-06T09:30:00Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ReportBadBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "team:manage or notifications:manage"
      }
    },
    "/v1/report-schedules/{id}/recipients/{rid}": {
      "delete": {
        "tags": [
          "insight"
        ],
        "summary": "Remove a recipient",
        "description": "**Required scope:** `team:manage` or `notifications:manage`\n\nScope team:manage or notifications:manage. The recipient row is deleted: the address gets no further reports and its links stop working. Audited as `report_schedule.recipient_remove`.",
        "operationId": "removeReportRecipient",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Schedule id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "Recipient id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "team:manage or notifications:manage"
      }
    },
    "/v1/report-schedules/{id}/recipients/{rid}/resend": {
      "post": {
        "tags": [
          "insight"
        ],
        "summary": "Re-send the confirmation mail to a pending address (once per 10 minutes)",
        "description": "**Required scope:** `team:manage` or `notifications:manage`\n\nScope team:manage or notifications:manage. Issues a new confirmation link (the old one stops working, the new one\nis valid 7 days) and mails it. Only a `pending` recipient, at most once per 10 minutes, and within the tenant's 30\nconfirmation mails per 24 h — otherwise 429. The mail is sent in the background. Audited as\n`report_schedule.confirm_resend`.",
        "operationId": "resendReportConfirmation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Schedule id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "Recipient id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Sent (no body)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InsightInternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReportsUnavailable"
          }
        },
        "x-required-scope": "team:manage or notifications:manage"
      }
    },
    "/reports/confirm/{token}": {
      "parameters": [
        {
          "name": "token",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Double opt-in page of a report recipient…",
        "description": "Double opt-in page of a report recipient (shows a confirm button; a GET never confirms, so mail link scanners cannot)\n\nNo authentication: the token from the confirmation mail is the secret (valid 7 days). Shows the schedule, tenant and address with a button that POSTs to the same URL. The page language follows the schedule unless `lang` is given.",
        "operationId": "reportConfirmPage",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "description": "Page language",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown or expired token (HTML page)"
          },
          "503": {
            "description": "Scheduled reports not configured (HTML page)"
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "insight"
        ],
        "summary": "Confirm a report recipient",
        "description": "Makes the recipient active (idempotent; it receives the next report) and answers an HTML page with an unsubscribe link. Audited as `report_schedule.recipient_confirm` with the masked address.",
        "operationId": "reportConfirm",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "description": "Page language",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown or expired token (HTML page)"
          },
          "503": {
            "description": "Scheduled reports not configured (HTML page)"
          }
        },
        "security": []
      }
    },
    "/reports/unsubscribe/{token}": {
      "parameters": [
        {
          "name": "token",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "Unsubscribe page linked from every report mail (shows a button; a GET never unsubscribes)",
        "description": "No authentication: the per-recipient token is the secret. Shows the schedule, tenant and address with an Unsubscribe button (POST to the same URL), or says the address is already unsubscribed.",
        "operationId": "reportUnsubscribePage",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "description": "Page language",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown token (HTML page)"
          },
          "503": {
            "description": "Scheduled reports not configured (HTML page)"
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "insight"
        ],
        "summary": "Unsubscribe (also the RFC 8058 one-click target of the List-Unsubscribe-Post header)",
        "description": "Marks the recipient unsubscribed (idempotent; no further mails of this schedule) and answers an HTML page. Audited as `report_schedule.unsubscribe` with the masked address.",
        "operationId": "reportUnsubscribe",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "description": "Page language",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown token (HTML page)"
          },
          "503": {
            "description": "Scheduled reports not configured (HTML page)"
          }
        },
        "security": []
      }
    },
    "/exports/stats/{id}": {
      "get": {
        "tags": [
          "insight"
        ],
        "summary": "insight-v2 2.3: signed download link of a prepared statistics export…",
        "description": "insight-v2 2.3: signed download link of a prepared statistics export (from GET /v1/stats/exports; valid 15 minutes, no session)\n\nNo authentication: `exp` and `sig` (an HMAC with the export's own secret) are the credential, as handed out in\n`download_url`. Streams the gzip file as an attachment (curl, download managers). An invalid or expired link is\na plain-text 403; ask the list or get route for a fresh one. Audited as `stats.export_download` (signed link).",
        "operationId": "signedStatsExport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Export id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "exp",
            "in": "query",
            "required": true,
            "description": "Expiry, Unix seconds",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sig",
            "in": "query",
            "required": true,
            "description": "Signature from `download_url`",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The gzip file (attachment)",
            "content": {
              "application/gzip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "Invalid or expired link (plain text)"
          },
          "404": {
            "description": "Unknown export or exports not configured (plain text), or the file is no longer in storage (problem+json)"
          },
          "409": {
            "description": "The file is not ready (problem+json `conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/prewarm": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Pre-warm the edges for an asset, clip, channel or explicit URLs (scope `prewarm`, 6/min per tenant)",
        "description": "**Required scope:** `prewarm`\n\nSend exactly one of `asset_id`, `clip_id`, `channel_id` or `urls` (≤ 500, each on the tenant's CDN hostname).\nCDN-only tenants may send only `urls` (others → 403 `feature_disabled`). An asset must be `ready` (else 409).\nThe run is asynchronous: a `prewarm` job fans out to every enabled edge — masters are crawled\nthrough the edge, the top two renditions plus audio get their first 3 segments (last 3 for live), lower rungs only\ntheir playlists. Poll `GET /v1/prewarm/{id}` for per-edge results. Idempotent per (trigger, target) for 5 minutes:\na repeat returns the existing run with `created: false` and queues nothing. Limited to 6 requests per minute per\ntenant (counted before de-duplication). Audited as `prewarm.create`.",
        "operationId": "postPrewarm",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PrewarmRequest"
              },
              "example": {
                "asset_id": "0192a1b2-0000-7000-8000-000000000001"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Run queued (`created: true`), or the run already created for the same target in the last 5 minutes (`created: false`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "done",
                        "partial",
                        "failed"
                      ]
                    },
                    "created": {
                      "type": "boolean",
                      "description": "false when an existing run was returned (5-minute de-duplication)"
                    },
                    "run": {
                      "$ref": "#/components/schemas/PrewarmRun"
                    }
                  }
                },
                "example": {
                  "id": "0192a1b2-5a10-7c4b-9a1b-3c5d7e9f1a01",
                  "status": "queued",
                  "created": true,
                  "run": {
                    "id": "0192a1b2-5a10-7c4b-9a1b-3c5d7e9f1a01",
                    "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                    "trigger": "manual",
                    "target": "asset:0192a1b2-0000-7000-8000-000000000001",
                    "urls": [
                      "/vod/tv10poc/0192a1b2-0000-7000-8000-000000000001/master.m3u8"
                    ],
                    "status": "queued",
                    "results": null,
                    "created_at": "2026-10-06T08:15:00Z",
                    "finished_at": null
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset, clip or channel in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The asset is not `ready` yet (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Not exactly one target, more than 500 urls, or a URL not on the tenant's CDN hostname (`validation_error`, `errors[].field` = `urls[i]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "More than 6 pre-warm requests per minute for this tenant (or the general 20 requests/second limit); `Retry-After` in seconds",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Could not create the run",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "prewarm"
      }
    },
    "/v1/prewarm/{id}": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "One pre-warm run with per-edge results",
        "description": "**Required scope:** `prewarm`\n\nReturns a run created by `POST /v1/prewarm` (or by an automatic trigger: asset/clip ready, CMS publish, channel start).\n`status` moves `queued` → `running` → `done` | `partial` (some URL failed on some edge) | `failed`; `results` holds one\nentry per edge once the run finished, and `urls` lists every planned path once the run has been planned (the seed\npaths before that). Runs of other tenants answer 404.",
        "operationId": "getPrewarm",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Run id (a malformed id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Run",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrewarmRun"
                },
                "example": {
                  "id": "0192a1b2-5a10-7c4b-9a1b-3c5d7e9f1a01",
                  "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                  "trigger": "manual",
                  "target": "asset:0192a1b2-0000-7000-8000-000000000001",
                  "urls": [
                    "/vod/tv10poc/0192a1b2-0000-7000-8000-000000000001/master.m3u8",
                    "/vod/tv10poc/0192a1b2-0000-7000-8000-000000000001/1080p.m3u8",
                    "/vod/tv10poc/0192a1b2-0000-7000-8000-000000000001/1080p/seg_00001.m4s"
                  ],
                  "status": "done",
                  "results": [
                    {
                      "edge": "edge-fornax",
                      "site": "fornax",
                      "ok": 14,
                      "failed": 0,
                      "hits": 3,
                      "ms": 412,
                      "errors": []
                    }
                  ],
                  "sites": [
                    {
                      "site": "fornax",
                      "edges": 1,
                      "ok": 14,
                      "failed": 0,
                      "complete": true
                    }
                  ],
                  "created_at": "2026-10-06T08:15:00Z",
                  "finished_at": "2026-10-06T08:15:02Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "prewarm"
      }
    },
    "/v1/purge": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Purge edge caches by URLs, path prefix, asset, clip, tags or distribution (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nSend one of (checked in this order) `urls` (≤ 5000; each on the tenant CDN hostname or on one of its distribution\nhostnames — query strings and, on `hmac` distributions, the path token are dropped because they are not part of\nthe cache key), `prefix` (`/vod/<tenant>/…` or `/rec/<tenant>/…`, must end with `/`), `asset_id` (everything under\n`/vod/<tenant>/<asset id>/`), `clip_id` (the clip's master, DASH and per-rendition manifests), `distribution_id`\n(everything cached for that hostname, same as `POST /v1/distributions/{id}/purge`: answers 202 with the new\n`cache_generation`, no job) or `tags` (≤ 100; objects whose origin response carried that `Cache-Tag` /\n`Surrogate-Key`, on the tenant's hostnames with `cache_rules.tag_capture`).\nAsynchronous: queues a fast-priority `purge` job on every edge and answers 202 with its id — poll `GET /v1/jobs/{id}`\n(allowed with `delivery:write`) for per-edge results. Origin objects are never deleted. Audited as `purge.create`.",
        "operationId": "postPurge",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Exactly one field is used; when several are sent the first in the order urls, prefix, asset_id, clip_id, distribution_id, tags wins.",
                "properties": {
                  "tags": {
                    "type": "array",
                    "maxItems": 100,
                    "items": {
                      "type": "string",
                      "pattern": "^[A-Za-z0-9_.:/=-]{1,128}$"
                    },
                    "description": "Cache tags"
                  },
                  "distribution_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Purge everything cached for this distribution"
                  },
                  "urls": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uri"
                    },
                    "maxItems": 5000,
                    "description": "Absolute URLs on the tenant CDN hostname or a distribution hostname"
                  },
                  "prefix": {
                    "type": "string",
                    "description": "Edge path prefix `/vod/<tenant>/…/` or `/rec/<tenant>/…/` (trailing slash, no `..`)"
                  },
                  "asset_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "clip_id": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              },
              "example": {
                "urls": [
                  "https://cdn.tv10-poc.vustream.net/vod/tv10poc/0192a1b2-0000-7000-8000-000000000001/master.m3u8"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Purge job queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Poll `GET /v1/jobs/{id}`"
                    },
                    "keys": {
                      "type": "integer",
                      "description": "Number of exact cache keys purged (urls",
                      "clip)": null
                    },
                    "prefix": {
                      "type": "string",
                      "description": "The prefix purged (prefix",
                      "asset); empty otherwise": null
                    },
                    "hostname": {
                      "type": "string",
                      "description": "distribution_id only"
                    },
                    "cache_generation": {
                      "type": "integer",
                      "description": "distribution_id only"
                    },
                    "effective_within_s": {
                      "type": "integer",
                      "description": "distribution_id only: the edges pick the new generation up within this many seconds"
                    }
                  }
                },
                "example": {
                  "job_id": "0192a1b2-6b20-7c4b-9a1b-3c5d7e9f1a02",
                  "keys": 1,
                  "prefix": ""
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such asset, clip or distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "No target, more than 5000 urls, a URL on a foreign host, a prefix outside the tenant, more than 100 tags or an invalid tag (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Could not load distributions or create the job",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-partners": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Partner CDN accounts of the tenant (CDN77, BunnyCDN) with status, health, zone and shield host",
        "description": "**Required scope:** `stats:read`\n\nPartner CDN accounts of the tenant (CDN77, BunnyCDN) with status, health, zone and shield host; credentials never returned (scope `stats:read`)\n\nLists the tenant's partner CDN accounts and the providers that can be added. Each item has\nthe connection `status`, the probe `health`, the pull `zone` (when connected), the tenant's shield hostname the zone\npulls from, an Interhost kill switch (`killed`, `kill_reason`) and `usage_24h` (bytes the provider reported for the last\n24 hours; `source: none` when it reported nothing). API keys, shield and token secrets are never returned.\nAnswers 503 when partner CDNs are not enabled on this node.",
        "operationId": "listCDNPartners",
        "responses": {
          "200": {
            "description": "Partners and the provider catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CDNPartner"
                      }
                    },
                    "providers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CDNPartnerProvider"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192c3d4-1a2b-7c4b-9a1b-3c5d7e9f2b01",
                      "provider": "bunny",
                      "name": "BunnyCDN",
                      "pathway_id": "bunny",
                      "credentials_updated_at": "2026-10-01T09:00:00Z",
                      "token_auth": false,
                      "status": "connected",
                      "status_detail": "",
                      "health": "ok",
                      "health_detail": "",
                      "health_at": "2026-10-06T08:10:00Z",
                      "shield_ready": true,
                      "enabled": true,
                      "connected_at": "2026-10-01T09:01:12Z",
                      "disconnected_at": null,
                      "created_at": "2026-10-01T09:00:00Z",
                      "updated_at": "2026-10-06T08:10:00Z",
                      "zone": {
                        "id": "1874512",
                        "host": "vs-tv10poc-9f2b01.b-cdn.net",
                        "created_by_us": true
                      },
                      "shield_host": "tv10poc.shield.viewstream.co.il",
                      "killed": false,
                      "kill_reason": "",
                      "usage_24h": {
                        "bytes": 48210399232,
                        "source": "provider"
                      }
                    }
                  ],
                  "providers": [
                    {
                      "id": "bunny",
                      "name": "BunnyCDN",
                      "credential_fields": [
                        "api_key"
                      ]
                    },
                    {
                      "id": "cdn77",
                      "name": "CDN77",
                      "credential_fields": [
                        "api_token"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Add a partner CDN account",
        "description": "**Required scope:** `delivery:write`\n\nAdd a partner CDN account — `{provider: bunny|cdn77, name?, pathway_id?, credentials: {api_key|api_token}}`; the key is verified with the provider and sealed (scope `delivery:write`)\n\nRegisters a partner CDN account; it carries no traffic until `POST /v1/cdn-partners/{id}/connect` sets up the pull zone.\n`credentials` holds the provider's one field (`api_key` for bunny, `api_token` for cdn77; ≤ 512 characters, no\nwhitespace). The key is verified with the provider (up to 15 s) before it is sealed; a rejected key answers 422.\n`name` defaults to the provider name (≤ 80 characters); `pathway_id` defaults to the provider id (`bunny`, `bunny-2`, …).\nAt most 5 partners per tenant (409). New partners start with `status: new`. Audited as `cdn_partner.create`.",
        "operationId": "createCDNPartner",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "provider",
                  "credentials"
                ],
                "properties": {
                  "provider": {
                    "type": "string",
                    "enum": [
                      "bunny",
                      "cdn77"
                    ]
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Display name; default BunnyCDN / CDN77"
                  },
                  "pathway_id": {
                    "type": "string",
                    "pattern": "^[a-z][a-z0-9-]{1,23}$",
                    "description": "Steering pathway id (not `il`); default the provider id, then `<provider>-2`, …"
                  },
                  "credentials": {
                    "type": "object",
                    "description": "The provider's API key: `{api_key}` for bunny, `{api_token}` for cdn77. Write-only.",
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 512
                    }
                  }
                }
              },
              "example": {
                "provider": "bunny",
                "name": "BunnyCDN",
                "credentials": {
                  "api_key": "<bunny account API key>"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created (status `new`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CDNPartner"
                },
                "example": {
                  "id": "0192c3d4-1a2b-7c4b-9a1b-3c5d7e9f2b01",
                  "provider": "bunny",
                  "name": "BunnyCDN",
                  "pathway_id": "bunny",
                  "credentials_updated_at": "2026-10-01T09:00:00Z",
                  "token_auth": false,
                  "status": "connected",
                  "status_detail": "",
                  "health": "ok",
                  "health_detail": "",
                  "health_at": "2026-10-06T08:10:00Z",
                  "shield_ready": true,
                  "enabled": true,
                  "connected_at": "2026-10-01T09:01:12Z",
                  "disconnected_at": null,
                  "created_at": "2026-10-01T09:00:00Z",
                  "updated_at": "2026-10-06T08:10:00Z",
                  "zone": {
                    "id": "1874512",
                    "host": "vs-tv10poc-9f2b01.b-cdn.net",
                    "created_by_us": true
                  },
                  "shield_host": "tv10poc.shield.viewstream.co.il",
                  "killed": false,
                  "kill_reason": "",
                  "usage_24h": {
                    "bytes": 48210399232,
                    "source": "provider"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The tenant already has 5 partner CDNs, or already has this pathway id (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Unknown provider, missing/invalid key, name too long, bad pathway_id, or the provider rejected the key (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-partners/{id}": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "One partner CDN account (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nOne partner CDN account of the tenant, as in the list (status, health, zone, shield host, kill switch, 24-hour usage).\nCredentials are never returned.",
        "operationId": "getCDNPartner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Partner CDN id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The partner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CDNPartner"
                },
                "example": {
                  "id": "0192c3d4-1a2b-7c4b-9a1b-3c5d7e9f2b01",
                  "provider": "bunny",
                  "name": "BunnyCDN",
                  "pathway_id": "bunny",
                  "credentials_updated_at": "2026-10-01T09:00:00Z",
                  "token_auth": false,
                  "status": "connected",
                  "status_detail": "",
                  "health": "ok",
                  "health_detail": "",
                  "health_at": "2026-10-06T08:10:00Z",
                  "shield_ready": true,
                  "enabled": true,
                  "connected_at": "2026-10-01T09:01:12Z",
                  "disconnected_at": null,
                  "created_at": "2026-10-01T09:00:00Z",
                  "updated_at": "2026-10-06T08:10:00Z",
                  "zone": {
                    "id": "1874512",
                    "host": "vs-tv10poc-9f2b01.b-cdn.net",
                    "created_by_us": true
                  },
                  "shield_host": "tv10poc.shield.viewstream.co.il",
                  "killed": false,
                  "kill_reason": "",
                  "usage_24h": {
                    "bytes": 48210399232,
                    "source": "provider"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such partner CDN in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      },
      "patch": {
        "tags": [
          "delivery"
        ],
        "summary": "Rename, enable/disable, or pick the Bunny token scheme (`sha256|hs256`) (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nPartial update: `name` (1–80 characters), `enabled` (a disabled partner is left out of steering; the zone stays) and\n`token_scheme` (bunny: `sha256` or `hs256`; cdn77: `md5path` only). The token scheme is not echoed in the response.\nThe tenant's steering document is re-published within seconds. Audited as `cdn_partner.update`.",
        "operationId": "patchCDNPartner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Partner CDN id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "token_scheme": {
                    "type": "string",
                    "enum": [
                      "sha256",
                      "hs256",
                      "md5path"
                    ],
                    "description": "bunny: sha256 | hs256; cdn77: md5path"
                  }
                }
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated partner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CDNPartner"
                },
                "example": {
                  "id": "0192c3d4-1a2b-7c4b-9a1b-3c5d7e9f2b01",
                  "provider": "bunny",
                  "name": "BunnyCDN",
                  "pathway_id": "bunny",
                  "credentials_updated_at": "2026-10-01T09:00:00Z",
                  "token_auth": false,
                  "status": "connected",
                  "status_detail": "",
                  "health": "ok",
                  "health_detail": "",
                  "health_at": "2026-10-06T08:10:00Z",
                  "shield_ready": true,
                  "enabled": true,
                  "connected_at": "2026-10-01T09:01:12Z",
                  "disconnected_at": null,
                  "created_at": "2026-10-01T09:00:00Z",
                  "updated_at": "2026-10-06T08:10:00Z",
                  "zone": {
                    "id": "1874512",
                    "host": "vs-tv10poc-9f2b01.b-cdn.net",
                    "created_by_us": true
                  },
                  "shield_host": "tv10poc.shield.viewstream.co.il",
                  "killed": false,
                  "kill_reason": "",
                  "usage_24h": {
                    "bytes": 48210399232,
                    "source": "provider"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such partner CDN in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Empty or too long name, or a token scheme the provider does not support (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      },
      "delete": {
        "tags": [
          "delivery"
        ],
        "summary": "Remove a partner account (the provider zone is kept) (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nDeletes the partner account and its sealed credentials. The pull zone at the provider is NOT deleted — call\n`POST …/disconnect` with `delete_zone: true` first to remove a zone ViewStream created. The pathway leaves the\nsteering document within seconds. Audited as `cdn_partner.delete`.",
        "operationId": "deleteCDNPartner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Partner CDN id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such partner CDN in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-partners/{id}/credentials": {
      "put": {
        "tags": [
          "delivery"
        ],
        "summary": "Replace (rotate) the provider API key — verified first, sealed, audited (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nReplaces the provider key (`{credentials: {api_key}}` for bunny, `{credentials: {api_token}}` for cdn77). The new key is\nverified with the provider (up to 15 s) before it replaces the old one; a rejected key answers 422 and nothing changes.\n`credentials_updated_at` moves to now; the zone is not reconfigured. Audited as `cdn_partner.credentials_rotate`.",
        "operationId": "putCDNPartnerCredentials",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Partner CDN id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "credentials"
                ],
                "properties": {
                  "credentials": {
                    "type": "object",
                    "description": "`{api_key}` (bunny) or `{api_token}` (cdn77), ≤ 512 characters, write-only",
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 512
                    }
                  }
                }
              },
              "example": {
                "credentials": {
                  "api_key": "<new bunny account API key>"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The partner with the new key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CDNPartner"
                },
                "example": {
                  "id": "0192c3d4-1a2b-7c4b-9a1b-3c5d7e9f2b01",
                  "provider": "bunny",
                  "name": "BunnyCDN",
                  "pathway_id": "bunny",
                  "credentials_updated_at": "2026-10-01T09:00:00Z",
                  "token_auth": false,
                  "status": "connected",
                  "status_detail": "",
                  "health": "ok",
                  "health_detail": "",
                  "health_at": "2026-10-06T08:10:00Z",
                  "shield_ready": true,
                  "enabled": true,
                  "connected_at": "2026-10-01T09:01:12Z",
                  "disconnected_at": null,
                  "created_at": "2026-10-01T09:00:00Z",
                  "updated_at": "2026-10-06T08:10:00Z",
                  "zone": {
                    "id": "1874512",
                    "host": "vs-tv10poc-9f2b01.b-cdn.net",
                    "created_by_us": true
                  },
                  "shield_host": "tv10poc.shield.viewstream.co.il",
                  "killed": false,
                  "kill_reason": "",
                  "usage_24h": {
                    "bytes": 48210399232,
                    "source": "provider"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such partner CDN in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Missing/invalid key, or the provider rejected it (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-partners/{id}/connect": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Create (or adopt `zone_id`) and configure the pull zone",
        "description": "**Required scope:** `delivery:write`\n\nCreate (or adopt `zone_id`) and configure the pull zone — origin = the tenant shield hostname with a secret header, HLS caching, CORS, token auth when the policies require tokens (scope `delivery:write`)\n\nSynchronously (up to 60 s at the provider) creates the pull zone `vs-<tenant>-<6 hex>`, or adopts and reconfigures\n`zone_id` (an existing zone of the account; a reconnect keeps the current zone). The zone pulls from the tenant's\nshield hostname with a shield secret header (generated once and kept). When some playback policy requires tokens the\nzone gets token authentication. Then the shield and the zone are probed (up to 25 s). The answer is 200 even when the\nprovider failed — then `status: error` with `status_detail`. Body optional. Audited as `cdn_partner.connect`.",
        "operationId": "connectCDNPartner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Partner CDN id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "zone_id": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9-]{1,64}$",
                    "description": "Adopt this existing zone / CDN resource of the provider account instead of creating one"
                  }
                }
              },
              "example": {
                "zone_id": "1874512"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The partner after the attempt (`status` connected or error)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CDNPartner"
                },
                "example": {
                  "id": "0192c3d4-1a2b-7c4b-9a1b-3c5d7e9f2b01",
                  "provider": "bunny",
                  "name": "BunnyCDN",
                  "pathway_id": "bunny",
                  "credentials_updated_at": "2026-10-01T09:00:00Z",
                  "token_auth": false,
                  "status": "connected",
                  "status_detail": "",
                  "health": "ok",
                  "health_detail": "",
                  "health_at": "2026-10-06T08:10:00Z",
                  "shield_ready": true,
                  "enabled": true,
                  "connected_at": "2026-10-01T09:01:12Z",
                  "disconnected_at": null,
                  "created_at": "2026-10-01T09:00:00Z",
                  "updated_at": "2026-10-06T08:10:00Z",
                  "zone": {
                    "id": "1874512",
                    "host": "vs-tv10poc-9f2b01.b-cdn.net",
                    "created_by_us": true
                  },
                  "shield_host": "tv10poc.shield.viewstream.co.il",
                  "killed": false,
                  "kill_reason": "",
                  "usage_24h": {
                    "bytes": 48210399232,
                    "source": "provider"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such partner CDN in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Malformed zone_id (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The partner's credentials could not be opened, or a store/sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Partner CDNs are not enabled on this node, or no shield domain is configured (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-partners/{id}/disconnect": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Stop steering to the partner",
        "description": "**Required scope:** `delivery:write`\n\nStop steering to the partner; `delete_zone: true` also deletes a zone ViewStream created (scope `delivery:write`)\n\nTakes the partner out of steering: `status: disconnected`, health reset, the shield secret and token key are\nforgotten (the shield hostname leaves the edge configuration at its next render). With `delete_zone: true` the zone is\nalso deleted at the provider (up to 30 s) — only a zone ViewStream created (409 for an adopted one; 502 when the\nprovider refuses, nothing changed). Body optional. The account and key stay. Audited as `cdn_partner.disconnect`.",
        "operationId": "disconnectCDNPartner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Partner CDN id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "delete_zone": {
                    "type": "boolean",
                    "default": false
                  }
                }
              },
              "example": {
                "delete_zone": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The disconnected partner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CDNPartner"
                },
                "example": {
                  "id": "0192c3d4-1a2b-7c4b-9a1b-3c5d7e9f2b01",
                  "provider": "bunny",
                  "name": "BunnyCDN",
                  "pathway_id": "bunny",
                  "credentials_updated_at": "2026-10-01T09:00:00Z",
                  "token_auth": false,
                  "status": "connected",
                  "status_detail": "",
                  "health": "ok",
                  "health_detail": "",
                  "health_at": "2026-10-06T08:10:00Z",
                  "shield_ready": true,
                  "enabled": true,
                  "connected_at": "2026-10-01T09:01:12Z",
                  "disconnected_at": null,
                  "created_at": "2026-10-01T09:00:00Z",
                  "updated_at": "2026-10-06T08:10:00Z",
                  "zone": {
                    "id": "1874512",
                    "host": "vs-tv10poc-9f2b01.b-cdn.net",
                    "created_by_us": true
                  },
                  "shield_host": "tv10poc.shield.viewstream.co.il",
                  "killed": false,
                  "kill_reason": "",
                  "usage_24h": {
                    "bytes": 48210399232,
                    "source": "provider"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such partner CDN in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`delete_zone` on a zone ViewStream did not create (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "The provider did not delete the zone (`internal_error`); nothing changed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-partners/{id}/check": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Probe the shield hostname and the zone now (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nRuns the health probe now (up to 25 s) instead of waiting for the periodic check: the tenant shield hostname on the\nedge and a request through the pull zone. Updates `health` (`ok`, or `failing` after a second failed probe),\n`health_detail`, `health_at` and `shield_ready`. A partner that was never connected is returned unchanged. No body.",
        "operationId": "checkCDNPartner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Partner CDN id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The partner with fresh health",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CDNPartner"
                },
                "example": {
                  "id": "0192c3d4-1a2b-7c4b-9a1b-3c5d7e9f2b01",
                  "provider": "bunny",
                  "name": "BunnyCDN",
                  "pathway_id": "bunny",
                  "credentials_updated_at": "2026-10-01T09:00:00Z",
                  "token_auth": false,
                  "status": "connected",
                  "status_detail": "",
                  "health": "ok",
                  "health_detail": "",
                  "health_at": "2026-10-06T08:10:00Z",
                  "shield_ready": true,
                  "enabled": true,
                  "connected_at": "2026-10-01T09:01:12Z",
                  "disconnected_at": null,
                  "created_at": "2026-10-01T09:00:00Z",
                  "updated_at": "2026-10-06T08:10:00Z",
                  "zone": {
                    "id": "1874512",
                    "host": "vs-tv10poc-9f2b01.b-cdn.net",
                    "created_by_us": true
                  },
                  "shield_host": "tv10poc.shield.viewstream.co.il",
                  "killed": false,
                  "kill_reason": "",
                  "usage_24h": {
                    "bytes": 48210399232,
                    "source": "provider"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such partner CDN in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/steering": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Steering configuration, load-controller state, pathways with eligibility, overrides and the published…",
        "description": "**Required scope:** `stats:read`\n\nSteering configuration, load-controller state, pathways with eligibility, overrides and the published document (scope `stats:read`)\n\nThe tenant's content-steering setup: `config` as last saved (defaults when never saved —\noff, TTL 300 s), `state` of the overflow load controller (`active` = a steering document is published), every pathway\n(`il` = ViewStream's own edges, plus one per partner) with `eligible` and why not (disabled, not_connected, unhealthy,\nkilled, needs_token_auth, drm), overrides of the last 7 days, the published document's serial/TTL (null when none)\nand the `steer_url` players are given.",
        "operationId": "getSteering",
        "responses": {
          "200": {
            "description": "Steering overview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SteeringView"
                },
                "example": {
                  "config": {
                    "enabled": true,
                    "ttl_s": 300,
                    "geo_rules": [
                      {
                        "countries": [
                          "*"
                        ],
                        "except": [
                          "IL"
                        ],
                        "pathways": [
                          "bunny",
                          "il"
                        ]
                      }
                    ],
                    "load_rule": {
                      "enabled": true,
                      "scope": "tenant",
                      "metric": "gbps",
                      "threshold": 8,
                      "hysteresis_pct": 20,
                      "share_max": 30,
                      "ramp_step": 5,
                      "ramp_every_s": 60,
                      "min_hold_s": 300,
                      "pathways": [
                        "bunny"
                      ]
                    },
                    "weights": {
                      "bunny": 100
                    },
                    "updated_by": "key:ab12cd34",
                    "updated_at": "2026-10-05T12:00:00Z"
                  },
                  "state": {
                    "active": true,
                    "share": 0,
                    "phase": "idle",
                    "since": null,
                    "measured": 2.4,
                    "measured_unit": "gbps",
                    "capacity_gbps": 20,
                    "load_error": ""
                  },
                  "pathways": [
                    {
                      "id": "il",
                      "kind": "interhost",
                      "host": "cdn.tv10-poc.vustream.net",
                      "eligible": true,
                      "reason": ""
                    },
                    {
                      "id": "bunny",
                      "kind": "partner",
                      "provider": "bunny",
                      "host": "vs-tv10poc-9f2b01.b-cdn.net",
                      "eligible": true,
                      "reason": ""
                    }
                  ],
                  "overrides": [],
                  "doc": {
                    "serial": 42,
                    "updated_at": "2026-10-06T08:00:05Z",
                    "ttl": 300
                  },
                  "steer_url": "https://steer.viewstream.co.il/steer?c=tv10poc"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "delivery"
        ],
        "summary": "Change the steering configuration (partial)",
        "description": "**Required scope:** `delivery:write`\n\nChange the steering configuration (partial) — enabled, ttl_s, geo_rules, load_rule, weights (scope `delivery:write`)\n\nPartial update: only the fields sent change; `geo_rules`, `load_rule` and `weights` are replaced as a whole when sent.\nLimits: `ttl_s` 30–3600; ≤ 50 geo rules, each with 1–250 ISO alpha-2 `countries` (or `*`), optional `except`, ≥ 1 known\npathway; load rule `scope` tenant|overall, `metric` gbps|pct, `threshold` > 0 (pct ≤ 100), `hysteresis_pct` 0–90,\n`share_max`/`ramp_step` 1–100, `ramp_every_s` 15–3600, `min_hold_s` 0–86400, `pathways` = partner ids (unset fields get\nthe defaults); `weights` partner pathway → 0–1000. All errors are reported together. The controller re-publishes the\ndocument shortly after. Answers like `GET /v1/steering`. Audited as `steering.update`.",
        "operationId": "putSteering",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "ttl_s": {
                    "type": "integer",
                    "minimum": 30,
                    "maximum": 3600
                  },
                  "geo_rules": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "$ref": "#/components/schemas/SteeringGeoRule"
                    }
                  },
                  "load_rule": {
                    "$ref": "#/components/schemas/SteeringLoadRule"
                  },
                  "weights": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 1000
                    },
                    "description": "Partner pathway id → relative weight among partners"
                  }
                }
              },
              "example": {
                "enabled": true,
                "geo_rules": [
                  {
                    "countries": [
                      "*"
                    ],
                    "except": [
                      "IL"
                    ],
                    "pathways": [
                      "bunny",
                      "il"
                    ]
                  }
                ],
                "load_rule": {
                  "enabled": true,
                  "metric": "gbps",
                  "threshold": 8,
                  "pathways": [
                    "bunny"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The steering overview after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SteeringView"
                },
                "example": {
                  "config": {
                    "enabled": true,
                    "ttl_s": 300,
                    "geo_rules": [
                      {
                        "countries": [
                          "*"
                        ],
                        "except": [
                          "IL"
                        ],
                        "pathways": [
                          "bunny",
                          "il"
                        ]
                      }
                    ],
                    "load_rule": {
                      "enabled": true,
                      "scope": "tenant",
                      "metric": "gbps",
                      "threshold": 8,
                      "hysteresis_pct": 20,
                      "share_max": 30,
                      "ramp_step": 5,
                      "ramp_every_s": 60,
                      "min_hold_s": 300,
                      "pathways": [
                        "bunny"
                      ]
                    },
                    "weights": {
                      "bunny": 100
                    },
                    "updated_by": "key:ab12cd34",
                    "updated_at": "2026-10-05T12:00:00Z"
                  },
                  "state": {
                    "active": true,
                    "share": 0,
                    "phase": "idle",
                    "since": null,
                    "measured": 2.4,
                    "measured_unit": "gbps",
                    "capacity_gbps": 20,
                    "load_error": ""
                  },
                  "pathways": [
                    {
                      "id": "il",
                      "kind": "interhost",
                      "host": "cdn.tv10-poc.vustream.net",
                      "eligible": true,
                      "reason": ""
                    },
                    {
                      "id": "bunny",
                      "kind": "partner",
                      "provider": "bunny",
                      "host": "vs-tv10poc-9f2b01.b-cdn.net",
                      "eligible": true,
                      "reason": ""
                    }
                  ],
                  "overrides": [],
                  "doc": {
                    "serial": 42,
                    "updated_at": "2026-10-06T08:00:05Z",
                    "ttl": 300
                  },
                  "steer_url": "https://steer.viewstream.co.il/steer?c=tv10poc"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Out-of-range values, bad country codes or unknown pathways (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/steering/overrides": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Manual overrides (arm overflow / force pathway), last 30 days (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nManual overrides of the tenant that were active in the last 30 days or are scheduled. `state` is computed: scheduled,\nactive, ended or cancelled.",
        "operationId": "listSteeringOverrides",
        "responses": {
          "200": {
            "description": "Overrides",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SteeringOverride"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192c3d4-2c3d-7c4b-9a1b-3c5d7e9f2c01",
                      "kind": "arm_overflow",
                      "pathway": "bunny",
                      "share": 25,
                      "starts_at": "2026-10-10T17:00:00Z",
                      "ends_at": "2026-10-10T21:00:00Z",
                      "note": "Derby night",
                      "created_by": "key:ab12cd34",
                      "created_at": "2026-10-06T08:20:00Z",
                      "state": "scheduled"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Arm overflow or force a pathway now or scheduled",
        "description": "**Required scope:** `delivery:write`\n\nArm overflow or force a pathway now or scheduled — `{kind, pathway?, share?, starts_at?, ends_at}` (≤ 7 days) (scope `delivery:write`)\n\n`kind: arm_overflow` sends `share` % (1–100; when omitted the load rule's share applies) of new sessions to `pathway`\n(a partner; omit for every partner) for the window; `kind: force` puts `pathway` (`il` or a partner) first for everyone\nand ignores `share`. `starts_at` defaults to now (≤ 90 days ahead; a past time becomes now); `ends_at` is required,\nafter the start and at most 7 days later; `note` ≤ 200 characters. Audited as `steering.override_create`.",
        "operationId": "createSteeringOverride",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "ends_at"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "arm_overflow",
                      "force"
                    ]
                  },
                  "pathway": {
                    "type": "string",
                    "description": "force: required (il or a partner); arm_overflow: a partner, or omit for all partners"
                  },
                  "share": {
                    "type": "number",
                    "exclusiveMinimum": 0,
                    "maximum": 100,
                    "description": "arm_overflow only — percent of new sessions"
                  },
                  "starts_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "ends_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              },
              "example": {
                "kind": "arm_overflow",
                "pathway": "bunny",
                "share": 25,
                "starts_at": "2026-10-10T17:00:00Z",
                "ends_at": "2026-10-10T21:00:00Z",
                "note": "Derby night"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SteeringOverride"
                },
                "example": {
                  "id": "0192c3d4-2c3d-7c4b-9a1b-3c5d7e9f2c01",
                  "kind": "arm_overflow",
                  "pathway": "bunny",
                  "share": 25,
                  "starts_at": "2026-10-10T17:00:00Z",
                  "ends_at": "2026-10-10T21:00:00Z",
                  "note": "Derby night",
                  "created_by": "key:ab12cd34",
                  "created_at": "2026-10-06T08:20:00Z",
                  "state": "scheduled"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Bad kind, pathway, share or window (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/steering/overrides/{id}": {
      "delete": {
        "tags": [
          "delivery"
        ],
        "summary": "Cancel an override (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nCancels a scheduled or active override; its `state` becomes `cancelled` (the row is kept for history). Audited as\n`steering.override_cancel`.",
        "operationId": "cancelSteeringOverride",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Override id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Cancelled"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such override in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}/steering/simulate": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Evaluate a distribution's steering rules for a hypothetical request (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nThe same as `POST /v1/steering/simulate` with `distribution_id` = the path's distribution (v3 cdn-steering\n\"Decision log, simulation and statistics\"). Changes nothing.",
        "operationId": "simulateDistributionSteering",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$"
                  },
                  "sid": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "current": {
                    "type": "string",
                    "maxLength": 24
                  },
                  "distribution_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "evaluate that distribution's scope (else the tenant's)"
                  },
                  "asn": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "the viewer's AS number (0 = unknown)"
                  },
                  "ip": {
                    "type": "string",
                    "description": "the viewer's IPv4 / IPv6 address"
                  },
                  "path": {
                    "type": "string",
                    "maxLength": 2048,
                    "description": "URL path of the playback request (path conditions)"
                  },
                  "headers": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "maxProperties": 30
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "the moment to evaluate (window conditions); default now"
                  },
                  "draft": {
                    "type": "boolean",
                    "description": "use the scope's draft instead of its published rules"
                  },
                  "signals": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "boolean"
                    },
                    "description": "override live signals, e.g. {\"r:busy:traffic\": true}"
                  }
                }
              },
              "example": {
                "country": "IL",
                "asn": 12400,
                "ip": "10.2.3.4",
                "path": "/live/tv10poc/main/master.m3u8",
                "headers": {
                  "X-App": "tv"
                },
                "at": "2026-10-07T18:30:00Z",
                "draft": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The evaluation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "priority": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "default",
                        "force",
                        "sticky",
                        "rule",
                        "guard",
                        "geo",
                        "overflow",
                        "fallback"
                      ]
                    },
                    "rule": {
                      "type": "string",
                      "description": "the matched rule ('' = none: the v1 steps decided)"
                    },
                    "guard_applied": {
                      "type": "boolean",
                      "description": "the scope's max partner share sent the session to il"
                    },
                    "manifest": {
                      "$ref": "#/components/schemas/SteeringManifest"
                    },
                    "sid": {
                      "type": "string"
                    },
                    "steering_enabled": {
                      "type": "boolean"
                    },
                    "bucket": {
                      "type": "integer",
                      "description": "The session's overflow bucket"
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "distribution_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "host": {
                      "type": "string"
                    },
                    "draft": {
                      "type": "boolean"
                    },
                    "signals": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "boolean"
                      },
                      "description": "the live signals the rules saw"
                    }
                  }
                },
                "example": {
                  "priority": [
                    "il",
                    "bunny"
                  ],
                  "reason": "rule",
                  "rule": "office",
                  "guard_applied": false,
                  "manifest": {
                    "VERSION": 1,
                    "TTL": 300,
                    "RELOAD-URI": "https://steer.viewstream.co.il/steer?c=tv10poc&sid=k2m9Xq4tR8",
                    "PATHWAY-PRIORITY": [
                      "il",
                      "bunny"
                    ]
                  },
                  "sid": "k2m9Xq4tR8",
                  "steering_enabled": true,
                  "bucket": 37,
                  "at": "2026-10-07T18:30:00Z",
                  "host": "",
                  "draft": true,
                  "signals": {
                    "r:night:window": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Bad country code, IP, or a field too long (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/steering/simulate": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Evaluate the rules for a hypothetical request",
        "description": "**Required scope:** `stats:read`\n\nEvaluate the rules for a hypothetical request — priority, matched rule, guard rail and the manifest (scope `stats:read`)\n\nDry-runs the rules (even while steering is disabled — `steering_enabled` tells) for a hypothetical request: a viewer\nin `country` (ISO alpha-2; empty = unknown) with AS number `asn`, address `ip`, requesting `path` with `headers` at\ntime `at`, session id `sid` (generated when empty) currently on pathway `current`. With `distribution_id` the\ndistribution's rule scope is used (else the tenant's); `draft` evaluates the scope's draft instead of its published\nrules. Traffic and QoE conditions are unknown in a simulation (false) unless `signals` sets them. Returns the\npathway priority, the reason, the matched `rule`, whether a guard rail applied, the HLS steering manifest a player\nwould get and the session's bucket. Changes nothing.",
        "operationId": "simulateSteering",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$"
                  },
                  "sid": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "current": {
                    "type": "string",
                    "maxLength": 24
                  },
                  "distribution_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "evaluate that distribution's scope (else the tenant's)"
                  },
                  "asn": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "the viewer's AS number (0 = unknown)"
                  },
                  "ip": {
                    "type": "string",
                    "description": "the viewer's IPv4 / IPv6 address"
                  },
                  "path": {
                    "type": "string",
                    "maxLength": 2048,
                    "description": "URL path of the playback request (path conditions)"
                  },
                  "headers": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "maxProperties": 30
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "the moment to evaluate (window conditions); default now"
                  },
                  "draft": {
                    "type": "boolean",
                    "description": "use the scope's draft instead of its published rules"
                  },
                  "signals": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "boolean"
                    },
                    "description": "override live signals, e.g. {\"r:busy:traffic\": true}"
                  }
                }
              },
              "example": {
                "country": "IL",
                "asn": 12400,
                "ip": "10.2.3.4",
                "path": "/live/tv10poc/main/master.m3u8",
                "headers": {
                  "X-App": "tv"
                },
                "at": "2026-10-07T18:30:00Z",
                "draft": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The evaluation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "priority": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "default",
                        "force",
                        "sticky",
                        "rule",
                        "guard",
                        "geo",
                        "overflow",
                        "fallback"
                      ]
                    },
                    "rule": {
                      "type": "string",
                      "description": "the matched rule ('' = none: the v1 steps decided)"
                    },
                    "guard_applied": {
                      "type": "boolean",
                      "description": "the scope's max partner share sent the session to il"
                    },
                    "manifest": {
                      "$ref": "#/components/schemas/SteeringManifest"
                    },
                    "sid": {
                      "type": "string"
                    },
                    "steering_enabled": {
                      "type": "boolean"
                    },
                    "bucket": {
                      "type": "integer",
                      "description": "The session's overflow bucket"
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "distribution_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "host": {
                      "type": "string"
                    },
                    "draft": {
                      "type": "boolean"
                    },
                    "signals": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "boolean"
                      },
                      "description": "the live signals the rules saw"
                    }
                  }
                },
                "example": {
                  "priority": [
                    "il",
                    "bunny"
                  ],
                  "reason": "rule",
                  "rule": "office",
                  "guard_applied": false,
                  "manifest": {
                    "VERSION": 1,
                    "TTL": 300,
                    "RELOAD-URI": "https://steer.viewstream.co.il/steer?c=tv10poc&sid=k2m9Xq4tR8",
                    "PATHWAY-PRIORITY": [
                      "il",
                      "bunny"
                    ]
                  },
                  "sid": "k2m9Xq4tR8",
                  "steering_enabled": true,
                  "bucket": 37,
                  "at": "2026-10-07T18:30:00Z",
                  "host": "",
                  "draft": true,
                  "signals": {
                    "r:night:window": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Bad country code, IP, or a field too long (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/steering/rules": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "A scope's steering rules — draft, published set and versions (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nRules are ordered and the first enabled matching rule decides (v3 cdn-steering \"Selector rules\"). A scope is the\ntenant (no `distribution_id`) or one of its distributions; a request on a distribution's hostname uses that\ndistribution's rules, else the tenant's. With no published rules (or an empty set) steering behaves exactly as\nbefore (geo rules, load rule, overrides). `pathways` lists the ids a rule may route to.",
        "operationId": "getSteeringRules",
        "parameters": [
          {
            "name": "distribution_id",
            "in": "query",
            "description": "a distribution of the tenant; absent = the tenant's own rules",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The scope's rule sets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "distribution_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "host": {
                      "type": "string"
                    },
                    "draft": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SteeringRuleSet"
                        }
                      ]
                    },
                    "published": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SteeringRuleSet"
                        }
                      ]
                    },
                    "versions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SteeringRuleSet"
                      }
                    },
                    "pathways": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "delivery"
        ],
        "summary": "Save a scope's draft rule set (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nCreates or replaces the scope's draft (nothing changes for viewers until it is published). Validation: rule ids\n`^[a-z0-9][a-z0-9_-]{0,31}$` and unique, at most 100 rules, 1–8 pathways per rule with weights 1–100, pathways and\nfallbacks among the tenant's (`il` and its partner CDNs), countries ISO alpha-2, CIDRs valid, paths starting with `/`\n(`*` = anything), windows `HH:MM` (+ days 1–7, time zone) or `start`/`end`, guard `max_partner_share` 0–100.",
        "operationId": "putSteeringRules",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rules"
                ],
                "properties": {
                  "distribution_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "rules": {
                    "type": "array",
                    "maxItems": 100,
                    "items": {
                      "$ref": "#/components/schemas/SteeringRule"
                    }
                  },
                  "guard": {
                    "$ref": "#/components/schemas/SteeringGuard"
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              },
              "example": {
                "rules": [
                  {
                    "id": "tv-app",
                    "name": "TV app on live → Bunny",
                    "enabled": true,
                    "when": {
                      "paths": [
                        "/live/*"
                      ],
                      "headers": [
                        {
                          "name": "X-App",
                          "value": "tv"
                        }
                      ]
                    },
                    "then": {
                      "pathways": [
                        {
                          "id": "bunny",
                          "weight": 100
                        }
                      ],
                      "fallback": [
                        "il"
                      ]
                    }
                  },
                  {
                    "id": "evening",
                    "name": "Evening 80/20",
                    "enabled": true,
                    "when": {
                      "window": {
                        "days": [
                          1,
                          2,
                          3,
                          4,
                          5,
                          6,
                          7
                        ],
                        "from": "20:00",
                        "to": "23:30",
                        "tz": "Asia/Jerusalem"
                      }
                    },
                    "then": {
                      "pathways": [
                        {
                          "id": "il",
                          "weight": 80
                        },
                        {
                          "id": "bunny",
                          "weight": 20
                        }
                      ]
                    }
                  }
                ],
                "guard": {
                  "max_partner_share": 30
                },
                "note": "evening offload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved draft",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SteeringRuleSet"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid rule set (`validation_error`, each problem in `errors[]` with its field path)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/steering/rules/draft": {
      "delete": {
        "tags": [
          "delivery"
        ],
        "summary": "Discard a scope's draft rule set (scope `delivery:write`)",
        "operationId": "deleteSteeringRulesDraft",
        "parameters": [
          {
            "name": "distribution_id",
            "in": "query",
            "description": "a distribution of the tenant; absent = the tenant's own rules",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Discarded (or there was none)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write",
        "description": "**Required scope:** `delivery:write`"
      }
    },
    "/v1/steering/rules/publish": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Publish a scope's draft — it applies to new steering decisions within about 15 s (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nThe draft becomes the published version; the previous one is archived (rollback can bring it back). The steering\npublisher folds it into the tenant's document on its next tick (15 s), and the change is logged in the rule events.",
        "operationId": "publishSteeringRules",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "distribution_id": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The published set",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SteeringRuleSet"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "There is no draft to publish (`no_draft`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/steering/rules/rollback": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Publish a copy of an older version of a scope's rules (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nPublishes the rules of `version` as a new version (the current published set is archived; a draft is kept).",
        "operationId": "rollbackSteeringRules",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version"
                ],
                "properties": {
                  "distribution_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "version": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new published set",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SteeringRuleSet"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution, or no such published version",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "version is not a positive integer",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/steering/rules/events": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "The decision log's scheduled part — publishes, rollbacks and window activations (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nNewest first. `activated` / `deactivated` are logged by the publisher when a rule's time window opens or closes.\nPer-request decisions are counted in `GET /v1/steering/rules/stats`. `scope=all` lists every scope of the tenant.",
        "operationId": "getSteeringRuleEvents",
        "parameters": [
          {
            "name": "distribution_id",
            "in": "query",
            "description": "a distribution of the tenant; absent = the tenant's own rules",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "all = every scope",
            "schema": {
              "type": "string",
              "enum": [
                "all"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "distribution_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uuid"
                          },
                          "rule_id": {
                            "type": "string"
                          },
                          "event": {
                            "type": "string",
                            "enum": [
                              "published",
                              "rolled_back",
                              "activated",
                              "deactivated"
                            ]
                          },
                          "detail": {
                            "type": "object",
                            "additionalProperties": true
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution of the tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/steering/rules/stats": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Steering decisions per scope, rule, reason and first pathway over the last hours (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nFrom the counter `vs_steering_decisions_total` (every `/steer` answer and every issued session). `rule` is empty\nwhen no rule matched (the v1 steps decided). Without Prometheus the list is empty and `note` says why.",
        "operationId": "getSteeringRuleStats",
        "parameters": [
          {
            "name": "hours",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 744,
              "default": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Decision counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "hours": {
                      "type": "integer"
                    },
                    "source": {
                      "type": "string"
                    },
                    "note": {
                      "type": "string"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "scope": {
                            "type": "string",
                            "description": "tenant, or the distribution hostname"
                          },
                          "rule": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "pathway": {
                            "type": "string"
                          },
                          "decisions": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/steering/usage": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Sessions and bytes per pathway (`from`, `to` RFC 3339, ≤ 92 days)",
        "description": "**Required scope:** `stats:read`\n\nSessions and bytes per pathway (`from`, `to` RFC 3339, ≤ 92 days); partner bytes from the provider, else `estimated` (scope `stats:read`)\n\nSessions and delivered bytes per pathway for `from`–`to` (default the last 24 hours; at most 92 days). `il` bytes come\nfrom the edge logs (`bytes_source: edge`); partner bytes from the provider's reports (`provider`) when available, else\nestimated from player beacons (`estimated`); `none` when nothing is known. `share` is the pathway's share of sessions\n(0–1). Unparseable `from`/`to` values fall back to the defaults.",
        "operationId": "steeringUsage",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "RFC 3339; default now − 24 h",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "RFC 3339; default now",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage per pathway (`il` first, then the partners)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "pathways": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "sessions": {
                            "type": "integer"
                          },
                          "share": {
                            "type": "number"
                          },
                          "bytes": {
                            "type": "integer",
                            "format": "int64"
                          },
                          "bytes_source": {
                            "type": "string",
                            "enum": [
                              "edge",
                              "provider",
                              "estimated",
                              "none"
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "from": "2026-10-05T08:00:00Z",
                  "to": "2026-10-06T08:00:00Z",
                  "pathways": [
                    {
                      "id": "il",
                      "sessions": 18240,
                      "share": 0.91,
                      "bytes": 3912345678901,
                      "bytes_source": "edge"
                    },
                    {
                      "id": "bunny",
                      "sessions": 1804,
                      "share": 0.09,
                      "bytes": 48210399232,
                      "bytes_source": "provider"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`to` not after `from`, or a range over 92 days (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/distributions": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "List the tenant's CDN distributions (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nEvery hostname the edges serve for the tenant (including the primary CDN hostname, `primary: true`), with origin,\ntoken mode (only a `secret_hint`, never the secret), CORS, geo and cache rules. Also returns the CNAME target customers\npoint their hostnames at and the edge addresses to allow-list at an external origin. Not paginated (a tenant has few).\nAvailable to CDN-only tenants.",
        "operationId": "listDistributions",
        "responses": {
          "200": {
            "description": "Distributions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Distribution"
                      }
                    },
                    "cname_target": {
                      "type": "string"
                    },
                    "edge_ips": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192b7e1-4c2d-7c4b-9a1b-3c5d7e9f3a01",
                      "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                      "hostname": "video.example.co.il",
                      "origin_kind": "external",
                      "origin_url": "https://origin.example.co.il",
                      "origin_host_header": null,
                      "token_mode": "hmac",
                      "token_secret_prev_until": null,
                      "cors_origins": [
                        "https://www.example.co.il"
                      ],
                      "geo_allow": [],
                      "geo_deny": [],
                      "cache_rules": {
                        "query_keys": [
                          "lang"
                        ]
                      },
                      "enabled": true,
                      "created_at": "2026-10-06T08:00:00Z",
                      "updated_at": "2026-10-06T08:00:00Z",
                      "tenant_mode": "cdn",
                      "primary": false,
                      "secret_hint": "q8Zr1x…",
                      "prev_secret_active": false,
                      "cname_target": "cdn-poc.vustream.net",
                      "edge_ips": [
                        "185.37.148.250"
                      ]
                    }
                  ],
                  "cname_target": "cdn-poc.vustream.net",
                  "edge_ips": [
                    "185.37.148.250"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Create a distribution (scope `delivery:write`); an hmac/jwt secret is returned once",
        "description": "**Required scope:** `delivery:write`\n\n`hostname` (required, lower-case DNS name; Interhost's own zones and API hosts are refused). `origin_kind` is `library`\n(platform tenants only, the default for them: the ViewStream origin and live encoders, routed by path) or `external`\n(the default for CDN-only tenants; `origin_url` http(s)://host[:port] without path, credentials or query, optional\n`origin_host_header`; it must not be an internal name or resolve to private, Interhost or edge addresses).\n`token_mode` none (default) | hmac | jwt — a token mode returns the new `secret` once in this response. Limits: ≤ 50\n`cors_origins`, ISO alpha-2 geo lists, ≤ 10 cache `query_keys`, TTLs 0–31536000 s. `policy_id` is ignored here (attach\nwith PATCH). The edges pick the hostname up at their next render; the certificate is issued once the CNAME points at\n`cname_target`. Audited as `distribution.create`.",
        "operationId": "createDistribution",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DistributionInput"
              },
              "example": {
                "hostname": "video.example.co.il",
                "origin_kind": "external",
                "origin_url": "https://origin.example.co.il",
                "token_mode": "hmac",
                "cors_origins": [
                  "https://www.example.co.il"
                ],
                "cache_rules": {
                  "query_keys": [
                    "lang"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created (with `secret` once for hmac/jwt)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Distribution"
                },
                "example": {
                  "id": "0192b7e1-4c2d-7c4b-9a1b-3c5d7e9f3a01",
                  "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                  "hostname": "video.example.co.il",
                  "origin_kind": "external",
                  "origin_url": "https://origin.example.co.il",
                  "origin_host_header": null,
                  "token_mode": "hmac",
                  "token_secret_prev_until": null,
                  "cors_origins": [
                    "https://www.example.co.il"
                  ],
                  "geo_allow": [],
                  "geo_deny": [],
                  "cache_rules": {
                    "query_keys": [
                      "lang"
                    ]
                  },
                  "enabled": true,
                  "created_at": "2026-10-06T08:00:00Z",
                  "updated_at": "2026-10-06T08:00:00Z",
                  "tenant_mode": "cdn",
                  "primary": false,
                  "secret_hint": "q8Zr1x…",
                  "prev_secret_active": false,
                  "cname_target": "cdn-poc.vustream.net",
                  "edge_ips": [
                    "185.37.148.250"
                  ],
                  "secret": "q8Zr1xT0bW3mK9pL2vN7cY5dF4gH6jA1sE8uR0iO3k"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The hostname is already served by a distribution (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Missing/invalid/reserved hostname, bad origin, token mode, CORS origin, country code or cache rules (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "One distribution (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nOne distribution of the tenant (same shape as in the list; the secret itself is never returned).",
        "operationId": "getDistribution",
        "responses": {
          "200": {
            "description": "Distribution",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Distribution"
                },
                "example": {
                  "id": "0192b7e1-4c2d-7c4b-9a1b-3c5d7e9f3a01",
                  "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                  "hostname": "video.example.co.il",
                  "origin_kind": "external",
                  "origin_url": "https://origin.example.co.il",
                  "origin_host_header": null,
                  "token_mode": "hmac",
                  "token_secret_prev_until": null,
                  "cors_origins": [
                    "https://www.example.co.il"
                  ],
                  "geo_allow": [],
                  "geo_deny": [],
                  "cache_rules": {
                    "query_keys": [
                      "lang"
                    ]
                  },
                  "enabled": true,
                  "created_at": "2026-10-06T08:00:00Z",
                  "updated_at": "2026-10-06T08:00:00Z",
                  "tenant_mode": "cdn",
                  "primary": false,
                  "secret_hint": "q8Zr1x…",
                  "prev_secret_active": false,
                  "cname_target": "cdn-poc.vustream.net",
                  "edge_ips": [
                    "185.37.148.250"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      },
      "patch": {
        "tags": [
          "delivery"
        ],
        "summary": "Update a distribution (scope `delivery:write`); the hostname is immutable",
        "description": "**Required scope:** `delivery:write`\n\nPartial update with the create fields (omitted = unchanged; arrays replace). `hostname` cannot change (422). An\nunchanged external origin is not re-validated. Switching `token_mode` from `none` returns a new `secret` once;\nswitching to `none` drops the secrets. The tenant's primary hostname cannot be disabled (409). `policy_id` (uuid, or\nnull = inherit) attaches a playback policy to the distribution (also needs `delivery:write`; unknown policy → 404) and\nemits a `policy.changed` event. Audited as `distribution.update`.",
        "operationId": "patchDistribution",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/DistributionInput"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "policy_id": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "uuid",
                        "description": "Attach this playback policy to the distribution; null = inherit the tenant's"
                      }
                    }
                  }
                ]
              },
              "example": {
                "geo_allow": [
                  "IL"
                ],
                "cors_origins": [
                  "https://www.example.co.il",
                  "https://m.example.co.il"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated (with `secret` once when a token mode was just switched on)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Distribution"
                },
                "example": {
                  "id": "0192b7e1-4c2d-7c4b-9a1b-3c5d7e9f3a01",
                  "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                  "hostname": "video.example.co.il",
                  "origin_kind": "external",
                  "origin_url": "https://origin.example.co.il",
                  "origin_host_header": null,
                  "token_mode": "hmac",
                  "token_secret_prev_until": null,
                  "cors_origins": [
                    "https://www.example.co.il"
                  ],
                  "geo_allow": [],
                  "geo_deny": [],
                  "cache_rules": {
                    "query_keys": [
                      "lang"
                    ]
                  },
                  "enabled": true,
                  "created_at": "2026-10-06T08:00:00Z",
                  "updated_at": "2026-10-06T08:00:00Z",
                  "tenant_mode": "cdn",
                  "primary": false,
                  "secret_hint": "q8Zr1x…",
                  "prev_secret_active": false,
                  "cname_target": "cdn-poc.vustream.net",
                  "edge_ips": [
                    "185.37.148.250"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution, or no such policy for `policy_id` (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Disabling the tenant's primary hostname (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid field values, a hostname change, or a malformed policy_id (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      },
      "delete": {
        "tags": [
          "delivery"
        ],
        "summary": "Delete a distribution (scope `delivery:write`); not the tenant's primary hostname",
        "description": "**Required scope:** `delivery:write`\n\nRemoves the hostname from the edges at their next render. The tenant's primary CDN hostname cannot be deleted (409).\nAudited as `distribution.delete`.",
        "operationId": "deleteDistribution",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The tenant's primary hostname (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}/rotate-secret": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Rotate the token secret (scope `delivery:write`); the previous secret stays valid for `overlap_s`",
        "description": "**Required scope:** `delivery:write`\n\nGenerates a new token secret and returns it once in `secret`. The previous secret keeps verifying for `overlap_s`\nseconds (0–604800, default 3600; `prev_secret_active`, `token_secret_prev_until`); 0 revokes it immediately. Body\noptional. 409 when `token_mode` is `none`. Audited as `distribution.rotate_secret`.",
        "operationId": "rotateDistributionSecret",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "overlap_s": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 604800,
                    "default": 3600
                  }
                }
              },
              "example": {
                "overlap_s": 3600
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rotated (new `secret` once)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Distribution"
                },
                "example": {
                  "id": "0192b7e1-4c2d-7c4b-9a1b-3c5d7e9f3a01",
                  "customer_id": "0192a1b2-0000-7000-8000-0000000000aa",
                  "hostname": "video.example.co.il",
                  "origin_kind": "external",
                  "origin_url": "https://origin.example.co.il",
                  "origin_host_header": null,
                  "token_mode": "hmac",
                  "token_secret_prev_until": "2026-10-06T09:00:00Z",
                  "cors_origins": [
                    "https://www.example.co.il"
                  ],
                  "geo_allow": [],
                  "geo_deny": [],
                  "cache_rules": {
                    "query_keys": [
                      "lang"
                    ]
                  },
                  "enabled": true,
                  "created_at": "2026-10-06T08:00:00Z",
                  "updated_at": "2026-10-06T08:00:00Z",
                  "tenant_mode": "cdn",
                  "primary": false,
                  "secret_hint": "q8Zr1x…",
                  "prev_secret_active": true,
                  "cname_target": "cdn-poc.vustream.net",
                  "edge_ips": [
                    "185.37.148.250"
                  ],
                  "secret": "q8Zr1xT0bW3mK9pL2vN7cY5dF4gH6jA1sE8uR0iO3k"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Token mode is `none`; there is no secret (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "overlap_s out of range (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}/purge": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Purge everything cached for a distribution (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nv3 Bumps the distribution's `cache_generation`, which is part of every edge cache key of the hostname: the\nedges pick it up with the signed policy map (pulled every 5 s), after which every object is fetched from the origin\nagain. Old cache files are never read again and age out. No body; no job. Audited as `purge.distribution`.",
        "operationId": "purgeDistribution",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Generation bumped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "hostname": {
                      "type": "string"
                    },
                    "cache_generation": {
                      "type": "integer"
                    },
                    "effective_within_s": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "hostname": "video.example.co.il",
                  "cache_generation": 3,
                  "effective_within_s": 10
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}/log-deliveries": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Hourly raw log deliveries of a distribution, newest first (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nv3 One row per exported hour: `ok` (object written, `ref`), `empty` (no requests that hour, nothing written)\nor `failed` (`error`; retried up to 5 times, 10 minutes apart). An hour is exported 20–25 minutes after it\nends. Answers 503 when the log export is not available on this node.",
        "operationId": "listDistributionLogDeliveries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1–500, default 100",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "hour": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "ok",
                              "empty",
                              "failed"
                            ]
                          },
                          "rows": {
                            "type": "integer"
                          },
                          "bytes": {
                            "type": "integer"
                          },
                          "truncated": {
                            "type": "boolean",
                            "description": "The hour had more rows than one export allows"
                          },
                          "ref": {
                            "type": "string",
                            "description": "s3: bucket/key; sftp: the remote path"
                          },
                          "error": {
                            "type": "string"
                          },
                          "attempts": {
                            "type": "integer"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "hour": "2026-10-07T10:00:00Z",
                      "status": "ok",
                      "rows": 182344,
                      "bytes": 9123456,
                      "truncated": false,
                      "ref": "acme-logs/viewstream/video.example.co.il/dt=2026-10-07/hour=10.ndjson.gz",
                      "attempts": 1,
                      "updated_at": "2026-10-07T11:21:03Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The log export is not available on this node (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/distributions/{id}/config": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "A distribution's whole configuration as one JSON document (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nv3 The editable, non-secret configuration (`DistributionConfig`) and the number of its latest stored version\n(`0` = none recorded yet). Edit the document and send it back with `PUT` — a configuration round trip. Secrets\n(token, origin auth, log-export credentials) are never part of it; they stay write-only on `PATCH`.",
        "operationId": "getDistributionConfig",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The configuration document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "integer",
                      "description": "Latest stored version (0 = none yet); pass it back as `if_version`"
                    },
                    "config": {
                      "$ref": "#/components/schemas/DistributionConfig"
                    }
                  }
                },
                "example": {
                  "version": 3,
                  "config": {
                    "hostname": "video.example.co.il",
                    "origin_kind": "external",
                    "origin_url": "https://origin.example.co.il",
                    "origin_host_header": null,
                    "token_mode": "hmac",
                    "cors_origins": [
                      "https://www.example.co.il"
                    ],
                    "geo_allow": [],
                    "geo_deny": [],
                    "cache_rules": {
                      "preset": "streaming"
                    },
                    "enabled": true,
                    "origin_options": {},
                    "log_export": {}
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "delivery"
        ],
        "summary": "Replace a distribution's configuration with an edited document (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nv3 Sends back the whole document from `GET …/config`; every field is set (an omitted list or object = empty),\nvalidated exactly like `PATCH` (the hostname is immutable, 422). With `if_version`, the change is refused with 409\nwhen someone saved a newer version in between. Every successful save is stored as a new version (`GET …/versions`).\nSecrets are untouched. Audited as `distribution.update` with the `note`.",
        "operationId": "putDistributionConfig",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "config"
                ],
                "properties": {
                  "config": {
                    "$ref": "#/components/schemas/DistributionConfig"
                  },
                  "if_version": {
                    "type": "integer",
                    "description": "Refuse (409) unless this is still the latest version"
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Shown in the version history"
                  }
                }
              },
              "example": {
                "if_version": 3,
                "note": "allow Israel only",
                "config": {
                  "hostname": "video.example.co.il",
                  "origin_kind": "external",
                  "origin_url": "https://origin.example.co.il",
                  "origin_host_header": null,
                  "token_mode": "hmac",
                  "cors_origins": [
                    "https://www.example.co.il"
                  ],
                  "geo_allow": [
                    "IL"
                  ],
                  "geo_deny": [],
                  "cache_rules": {
                    "preset": "streaming"
                  },
                  "enabled": true,
                  "origin_options": {},
                  "log_export": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved (with `secret` once when a token mode was just switched on)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Distribution"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`if_version` is not the latest version, or disabling the tenant's primary hostname (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Missing `config`, invalid field values or a hostname change (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "`if_version` sent but configuration versions are not available on this node (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}/versions": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Stored configuration versions of a distribution, newest first (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nv3 Every save (create, `PATCH`, `PUT …/config`, rollback) stores the resulting `DistributionConfig` as the\nnext version, with who saved it and a note. The first change of a distribution created before versions existed also\nstores the previous configuration as a `baseline` version. `current` is the live configuration, for a diff.\nSecrets and the attached playback policy are not versioned. Answers 503 when versions are not available.",
        "operationId": "listDistributionVersions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1–100, default 50",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Versions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "version": {
                            "type": "integer"
                          },
                          "config": {
                            "$ref": "#/components/schemas/DistributionConfig"
                          },
                          "actor": {
                            "type": "string",
                            "description": "Who saved it (empty for a baseline)"
                          },
                          "note": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "current": {
                      "$ref": "#/components/schemas/DistributionConfig"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "version": 2,
                      "actor": "dana@example.co.il",
                      "note": "rollback to version 1",
                      "created_at": "2026-10-08T09:00:00Z",
                      "config": {
                        "hostname": "video.example.co.il",
                        "origin_kind": "external",
                        "origin_url": "https://origin.example.co.il",
                        "origin_host_header": null,
                        "token_mode": "hmac",
                        "cors_origins": [],
                        "geo_allow": [],
                        "geo_deny": [],
                        "cache_rules": {},
                        "enabled": true,
                        "origin_options": {},
                        "log_export": {}
                      }
                    }
                  ],
                  "current": {
                    "hostname": "video.example.co.il",
                    "origin_kind": "external",
                    "origin_url": "https://origin.example.co.il",
                    "origin_host_header": null,
                    "token_mode": "hmac",
                    "cors_origins": [],
                    "geo_allow": [],
                    "geo_deny": [],
                    "cache_rules": {},
                    "enabled": true,
                    "origin_options": {},
                    "log_export": {}
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Configuration versions are not available on this node (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/distributions/{id}/rollback": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Roll a distribution back to a stored configuration version (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nv3 Re-applies the stored `DistributionConfig` of `version`, validated like any change (an origin that no\nlonger resolves is refused with 422), and stores the result as a new version noted `rollback to version N` — a\nrollback can itself be rolled back. Secrets stay as they are; switching the token mode back on returns a new\n`secret` once. Audited as `distribution.update`.",
        "operationId": "rollbackDistribution",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version"
                ],
                "properties": {
                  "version": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              },
              "example": {
                "version": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rolled back",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Distribution"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution or version (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The version disables the tenant's primary hostname (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The stored configuration no longer validates (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Store or sealing error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Configuration versions are not available on this node (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/policies": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "Playback policies of the tenant, where each is attached, the tenant default and presets (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nLists every playback policy of the tenant, sorted by name, each with its normalised `rules`, whether it actually\nprotects anything (`protects`) and how many attach points use it (`attached`). Also returns the id of the tenant\ndefault policy (`null` = no protection by default), the one-click presets (today only `news`, which needs your\ndomains) and the size of the datacenter/hosting ASN list that `geo.block_datacenter` uses. Read-only.",
        "operationId": "listPolicies",
        "responses": {
          "200": {
            "description": "Policies, the tenant default, presets and the datacenter list size",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybackPolicy"
                      }
                    },
                    "default_policy_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid",
                      "description": "The policy attached to the tenant; null = no protection by default"
                    },
                    "presets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "enum": [
                              "news"
                            ]
                          },
                          "title": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "rules": {
                            "$ref": "#/components/schemas/PlaybackPolicyRules"
                          },
                          "needs": {
                            "type": "string",
                            "description": "What the preset needs in the create request (`domains`)"
                          }
                        }
                      }
                    },
                    "datacenter_asns": {
                      "type": "integer",
                      "description": "Number of ASNs on the datacenter/hosting list"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0199a3c4-5e60-7d21-9b8a-2f4c6e8a0b11",
                      "customer_id": "0199a3c4-1a20-7c10-8d4e-5b6c7d8e9f01",
                      "name": "Israel only",
                      "is_default": false,
                      "geo": {
                        "mode": "allow",
                        "countries": [
                          "IL"
                        ],
                        "deny_action": "slate"
                      },
                      "hotlink": {
                        "token": "off",
                        "ttl_live_s": 21600,
                        "ttl_vod_extra_s": 7200,
                        "bind": "none",
                        "allow_empty_referer": true,
                        "issue_rate_per_ip_min": 20
                      },
                      "drm": {
                        "mode": "none"
                      },
                      "version": 1,
                      "created_at": "2026-10-01T09:12:44Z",
                      "updated_at": "2026-10-01T09:12:44Z",
                      "rules": {
                        "geo": {
                          "mode": "allow",
                          "countries": [
                            "IL"
                          ],
                          "deny_action": "slate"
                        },
                        "hotlink": {
                          "token": "off",
                          "ttl_live_s": 21600,
                          "ttl_vod_extra_s": 7200,
                          "bind": "none",
                          "allow_empty_referer": true,
                          "issue_rate_per_ip_min": 20
                        },
                        "drm": {
                          "mode": "none"
                        }
                      },
                      "protects": true,
                      "attached": {
                        "tenant": false,
                        "channels": 1,
                        "assets": 0,
                        "distributions": 0
                      }
                    }
                  ],
                  "default_policy_id": null,
                  "presets": [
                    {
                      "name": "news",
                      "title": "Free-to-air news (brief §12.4)",
                      "description": "Short-lived playback tokens (never bound to the viewer's network: VPNs, iCloud Private Relay and CGNAT carriers play), referrer/CORS/embed limited to your domains, geo off, datacenter networks allowed (VPN users), no DRM.",
                      "rules": {
                        "geo": {
                          "mode": "off",
                          "deny_action": "403"
                        },
                        "hotlink": {
                          "token": "required",
                          "ttl_live_s": 21600,
                          "ttl_vod_extra_s": 7200,
                          "bind": "none",
                          "allow_empty_referer": true,
                          "issue_rate_per_ip_min": 20
                        },
                        "drm": {
                          "mode": "none"
                        }
                      },
                      "needs": "domains"
                    }
                  ],
                  "datacenter_asns": 726
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Create a playback policy (scope `delivery:write`); `preset news` + `domains` builds the brief template",
        "description": "**Required scope:** `delivery:write`\n\nCreates a named policy with up to three sections — `geo` (off/allow/deny by country, ASN deny list, datacenter\nblock, 403 or slate), `hotlink` (path tokens, TTLs, network binding, referrer, CORS, embed domains) and `drm`\n(`none`, or `aes128` HLS encryption, which requires `hotlink.token: required`; `multi` is refused until a DRM\nvendor is configured). Omitted sections get the defaults (everything off). With `preset: news` the rules are\nbuilt from `domains` first; explicit sections then replace the preset's. A new policy is not attached anywhere —\nuse `PUT /v1/policy-attachments`. Creates the tenant's playback signing key if it has none, audits\n`policy.create` and emits the `policy.changed` event. Body limit 64 KiB; names are unique per tenant (1–80 chars).",
        "operationId": "createPolicy",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaybackPolicyRequest"
              },
              "example": {
                "name": "Israel only",
                "geo": {
                  "mode": "allow",
                  "countries": [
                    "IL"
                  ],
                  "deny_action": "slate"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybackPolicy"
                },
                "example": {
                  "id": "0199a3c4-5e60-7d21-9b8a-2f4c6e8a0b11",
                  "customer_id": "0199a3c4-1a20-7c10-8d4e-5b6c7d8e9f01",
                  "name": "Israel only",
                  "is_default": false,
                  "geo": {
                    "mode": "allow",
                    "countries": [
                      "IL"
                    ],
                    "deny_action": "slate"
                  },
                  "hotlink": {
                    "token": "off",
                    "ttl_live_s": 21600,
                    "ttl_vod_extra_s": 7200,
                    "bind": "none",
                    "allow_empty_referer": true,
                    "issue_rate_per_ip_min": 20
                  },
                  "drm": {
                    "mode": "none"
                  },
                  "version": 1,
                  "created_at": "2026-10-01T09:12:44Z",
                  "updated_at": "2026-10-01T09:12:44Z",
                  "rules": {
                    "geo": {
                      "mode": "allow",
                      "countries": [
                        "IL"
                      ],
                      "deny_action": "slate"
                    },
                    "hotlink": {
                      "token": "off",
                      "ttl_live_s": 21600,
                      "ttl_vod_extra_s": 7200,
                      "bind": "none",
                      "allow_empty_referer": true,
                      "issue_rate_per_ip_min": 20
                    },
                    "drm": {
                      "mode": "none"
                    }
                  },
                  "protects": true,
                  "attached": {
                    "tenant": false,
                    "channels": 0,
                    "assets": 0,
                    "distributions": 0
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/policies/{id}": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "One playback policy (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nThe policy with its normalised rules and attach-point counts. A policy of another tenant answers 404 like an unknown id.",
        "operationId": "getPolicy",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Policy id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The policy",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybackPolicy"
                },
                "example": {
                  "id": "0199a3c4-5e60-7d21-9b8a-2f4c6e8a0b11",
                  "customer_id": "0199a3c4-1a20-7c10-8d4e-5b6c7d8e9f01",
                  "name": "Israel only",
                  "is_default": false,
                  "geo": {
                    "mode": "allow",
                    "countries": [
                      "IL"
                    ],
                    "deny_action": "slate"
                  },
                  "hotlink": {
                    "token": "off",
                    "ttl_live_s": 21600,
                    "ttl_vod_extra_s": 7200,
                    "bind": "none",
                    "allow_empty_referer": true,
                    "issue_rate_per_ip_min": 20
                  },
                  "drm": {
                    "mode": "none"
                  },
                  "version": 1,
                  "created_at": "2026-10-01T09:12:44Z",
                  "updated_at": "2026-10-01T09:12:44Z",
                  "rules": {
                    "geo": {
                      "mode": "allow",
                      "countries": [
                        "IL"
                      ],
                      "deny_action": "slate"
                    },
                    "hotlink": {
                      "token": "off",
                      "ttl_live_s": 21600,
                      "ttl_vod_extra_s": 7200,
                      "bind": "none",
                      "allow_empty_referer": true,
                      "issue_rate_per_ip_min": 20
                    },
                    "drm": {
                      "mode": "none"
                    }
                  },
                  "protects": true,
                  "attached": {
                    "tenant": false,
                    "channels": 1,
                    "assets": 0,
                    "distributions": 0
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      },
      "patch": {
        "tags": [
          "protection"
        ],
        "summary": "Change a policy — version + 1, the edges pick it up within 10 s (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nPartial update: `name` renames; a present `geo`, `hotlink` or `drm` object replaces that whole section (send the\ncomplete card); omitted sections are kept; `preset: news` + `domains` rebuilds the rules from the preset. The merged\nrules are validated as on create. The version goes up by one and the edges load the new map within about 10 s.\nChanging `drm.mode` on a policy that covers ready assets queues a re-encode job for each of them (see\n`GET /v1/policies/{id}/drm-impact` first). Audits `policy.update`; emits `policy.changed`.",
        "operationId": "patchPolicy",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Policy id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaybackPolicyRequest"
              },
              "example": {
                "hotlink": {
                  "token": "required",
                  "bind": "asn",
                  "referer_allow": [
                    "example.co.il",
                    "*.example.co.il"
                  ],
                  "embed_domains": [
                    "example.co.il"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The policy after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybackPolicy"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "delivery:write"
      },
      "delete": {
        "tags": [
          "protection"
        ],
        "summary": "Delete a policy; its attach points fall back to the tenant default (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nDeletes the policy; every channel, asset and distribution it was attached to inherits again (the tenant default,\nor no protection when the deleted policy was the default). If that changes the encryption of ready assets, they\nare queued for re-encoding. Audits `policy.delete`; emits `policy.changed`.",
        "operationId": "deletePolicy",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Policy id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/policies/{id}/test": {
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Simulate a request against a policy — decisions for a live, a VOD and a clip path (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nRuns the edge decision (token → geo/ASN/datacenter → referrer → CORS) for a sample live master, VOD master and\nclip master of the tenant, as if a viewer from `country` (default `IL`) on `asn` sent `referer`/`origin`.\n`token` picks the token case: `none` (no token), `valid`, `expired` or `other_network` (bound to a different\nnetwork). Uses a throw-away key; nothing is stored or changed. Body limit 16 KiB.",
        "operationId": "testPolicy",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Policy id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "country": {
                    "type": "string",
                    "description": "ISO 3166-1 alpha-2 of the simulated viewer; default IL",
                    "example": "NL"
                  },
                  "asn": {
                    "type": "integer",
                    "description": "Simulated viewer ASN (0 = unknown)",
                    "example": 16509
                  },
                  "referer": {
                    "type": "string",
                    "description": "Simulated Referer header"
                  },
                  "origin": {
                    "type": "string",
                    "description": "Simulated Origin header (decides the CORS answer)"
                  },
                  "token": {
                    "type": "string",
                    "enum": [
                      "none",
                      "valid",
                      "expired",
                      "other_network"
                    ],
                    "default": "none"
                  }
                }
              },
              "example": {
                "country": "NL",
                "asn": 16509,
                "referer": "https://www.example.co.il/live",
                "token": "valid"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decisions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "policy_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "version": {
                      "type": "integer"
                    },
                    "country": {
                      "type": "string"
                    },
                    "asn": {
                      "type": "integer"
                    },
                    "datacenter": {
                      "type": "boolean",
                      "description": "The ASN is on the datacenter/hosting list"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "live",
                              "vod",
                              "clip"
                            ]
                          },
                          "path": {
                            "type": "string"
                          },
                          "decision": {
                            "$ref": "#/components/schemas/PolicyDecision"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "policy_id": "0199a3c4-5e60-7d21-9b8a-2f4c6e8a0b11",
                  "version": 3,
                  "country": "NL",
                  "asn": 16509,
                  "datacenter": true,
                  "results": [
                    {
                      "kind": "live",
                      "path": "/live/tv10poc/main/master.m3u8",
                      "decision": {
                        "allow": false,
                        "reason": "geo",
                        "slate": true,
                        "detail": "country NL is not on the allow list",
                        "country": "NL",
                        "asn": 16509,
                        "datacenter": true,
                        "sid": "TEST",
                        "token_exp": 1759742400
                      }
                    },
                    {
                      "kind": "vod",
                      "path": "/vod/tv10poc/00000000-0000-0000-0000-000000000000/master.m3u8",
                      "decision": {
                        "allow": false,
                        "reason": "geo",
                        "slate": true,
                        "detail": "country NL is not on the allow list",
                        "country": "NL",
                        "asn": 16509,
                        "datacenter": true,
                        "sid": "TEST",
                        "token_exp": 1759742400
                      }
                    },
                    {
                      "kind": "clip",
                      "path": "/m/clips/00000000-0000-0000-0000-000000000000/master.m3u8",
                      "decision": {
                        "allow": false,
                        "reason": "geo",
                        "slate": true,
                        "detail": "country NL is not on the allow list",
                        "country": "NL",
                        "asn": 16509,
                        "datacenter": true,
                        "sid": "TEST",
                        "token_exp": 1759742400
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/policy-attachments": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The tenant's attach points that do not inherit (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nEvery attach point (tenant default, channel, asset, CDN distribution) that names a policy. Anything not listed inherits — channels and distributions from the tenant default, assets from the tenant default, clips from their channel.",
        "operationId": "listPolicyAttachments",
        "responses": {
          "200": {
            "description": "Attach points",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PolicyAttachment"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "kind": "tenant",
                      "target_id": "0199a3c4-1a20-7c10-8d4e-5b6c7d8e9f01",
                      "ref": "tv10poc",
                      "label": "tv10poc",
                      "policy_id": "0199a3c4-5e60-7d21-9b8a-2f4c6e8a0b11"
                    },
                    {
                      "kind": "channel",
                      "target_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                      "ref": "main",
                      "label": "ערוץ 10",
                      "policy_id": "0199a3c4-7a90-7e44-a1b2-c3d4e5f60718"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "protection"
        ],
        "summary": "Attach a policy to the tenant default, a channel, an asset or a distribution",
        "description": "**Required scope:** `delivery:write`\n\nAttach a policy to the tenant default, a channel, an asset or a distribution; `policy_id null` = inherit (scope `delivery:write`)\n\nSets one attach point. `kind: tenant` needs no `target_id` (it is the tenant itself); `channel`, `asset` and\n`distribution` need the target's id. `policy_id` null or omitted removes the attachment so the target inherits.\nThe edges apply it within about 10 s. Attaching to the tenant or an asset can change which ready assets are\nencrypted; those are queued for re-encoding. Audits `policy.attach`; emits `policy.changed` (`change: attach`).\nPATCH /v1/channels/{id}, /v1/assets/{id} and /v1/distributions/{id} accept `policy_id` too.",
        "operationId": "putPolicyAttachment",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "tenant",
                      "channel",
                      "asset",
                      "distribution"
                    ]
                  },
                  "target_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Channel, asset or distribution id (ignored for tenant)"
                  },
                  "policy_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "The policy to attach; null or omitted = inherit"
                  }
                }
              },
              "example": {
                "kind": "channel",
                "target_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                "policy_id": "0199a3c4-7a90-7e44-a1b2-c3d4e5f60718"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The attach points after the change",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PolicyAttachment"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "kind": "tenant",
                      "target_id": "0199a3c4-1a20-7c10-8d4e-5b6c7d8e9f01",
                      "ref": "tv10poc",
                      "label": "tv10poc",
                      "policy_id": "0199a3c4-5e60-7d21-9b8a-2f4c6e8a0b11"
                    },
                    {
                      "kind": "channel",
                      "target_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                      "ref": "main",
                      "label": "ערוץ 10",
                      "policy_id": "0199a3c4-7a90-7e44-a1b2-c3d4e5f60718"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such channel, asset, distribution or policy in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/playback/tokens": {
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "A tokenised playback URL for a customer backend.2 way 2 (scope `playback:sign`)",
        "description": "**Required scope:** `playback:sign`\n\nMints a path token for one target (name exactly one of `channel`, `asset`, `clip`, `catchup`, `startover`) under\nthe tenant's current signing key — always tokenised, even when the effective policy does not require tokens.\nToken: `base64url(JSON payload) \".\" base64url(HMAC-SHA256(key, payload bytes))`; payload `{v: 1, kid, tid\n(tenant slug), p (path prefix it is valid for), exp, nbf (now − 30 s), sid, b, geo}`; the URL is\n`https://<cdn host>/t/<token>/<path>`. `ttl_s` 0–604800 (0 = policy default: live `ttl_live_s`, VOD the asset's\nduration + `ttl_vod_extra_s`, clips 2 h + extra, catch-up/start-over 4 h + extra). The policy's `hotlink.bind`\nbinds the token to `viewer_ip` (`ip_prefix`: its /24 or /48) or the viewer's ASN (`viewer_asn`, else looked up\nfrom `viewer_ip`). `viewer_id` (your subscriber id): when the policy sets `hotlink.max_streams`, the token\ncarries `vid` (a per-tenant hash of it, never the id itself) and `cs` (the limit), and the edges refuse that\nviewer's sessions beyond the limit (`X-VS-Deny: concurrent_streams`). Body limit 16 KiB.",
        "operationId": "postPlaybackToken",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/PlaybackTarget"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "ttl_s": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 604800,
                        "default": 0,
                        "description": "Token lifetime; 0 = the policy default for the target"
                      },
                      "viewer_ip": {
                        "type": "string",
                        "description": "The viewer's IP address (binds the token to its network when the policy asks for it)"
                      },
                      "viewer_asn": {
                        "type": "integer",
                        "description": "The viewer's ASN for bind: asn (default: looked up from viewer_ip)"
                      },
                      "viewer_id": {
                        "type": "string",
                        "description": "Your viewer/subscriber id; with the policy's max_streams it limits that viewer's concurrent sessions (only a hash reaches the edge)"
                      }
                    }
                  }
                ]
              },
              "example": {
                "channel": "main",
                "ttl_s": 21600,
                "viewer_ip": "203.0.113.24"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A tokenised playback URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "src": {
                      "type": "string",
                      "format": "uri",
                      "description": "The URL to give the player"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Same as src"
                    },
                    "token": {
                      "type": "string"
                    },
                    "sid": {
                      "type": "string",
                      "description": "Playback session id (ULID; appears in statistics, leak reports and revocations)"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "protected": {
                      "type": "boolean",
                      "description": "Always true here"
                    },
                    "thumbs": {
                      "type": "string",
                      "description": "Live, catch-up and start-over: base URL of the seek-bar preview frames (own token, same sid and expiry); append <yyyymmdd>/<hhmm>/<epoch_s>.jpg"
                    }
                  }
                },
                "example": {
                  "src": "https://cdn.tv10poc.vustream.net/t/EXAMPLE-PAYLOAD.EXAMPLE-SIGNATURE/live/tv10poc/main/master.m3u8",
                  "url": "https://cdn.tv10poc.vustream.net/t/EXAMPLE-PAYLOAD.EXAMPLE-SIGNATURE/live/tv10poc/main/master.m3u8",
                  "token": "EXAMPLE-PAYLOAD.EXAMPLE-SIGNATURE",
                  "sid": "01K6WQ8Z3T9VJ4M7N0C5R1XWDA",
                  "expires_at": "2026-10-06T14:00:00Z",
                  "protected": true,
                  "thumbs": "https://cdn.tv10poc.vustream.net/t/EXAMPLE-THUMBS-PAYLOAD.EXAMPLE-SIGNATURE/rec/tv10poc/main/thumbs/"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Invalid ttl_s or viewer_ip, no target named, or the channel/asset does not exist in this tenant (validation_error; the detail says which)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "playback:sign"
      }
    },
    "/steer": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "HLS content-steering manifest (steer.viewstream.co.il; public, CORS, no-store)",
        "description": "Change multi-cdn-steering. Answers the HLS content-steering manifest `{VERSION, TTL, RELOAD-URI, PATHWAY-PRIORITY,\nPATHWAY-CLONES}` for tenant `c` from its published steering document (geo rules, overflow, forced pathway, sticky\nsessions by `sid`; the document is cached 5 s). No authentication; `Access-Control-Allow-Origin: *`; `no-store`.\nIt never fails: an unknown tenant, no document or any error answers the safe manifest\n`{\"VERSION\":1,\"TTL\":300,\"PATHWAY-PRIORITY\":[\"il\"]}`. Players add `_HLS_pathway` and `_HLS_throughput`\n(RFC 8216bis); ViewStream Player v2 also sends `sid`, which is kept on the RELOAD-URI.",
        "operationId": "getSteer",
        "parameters": [
          {
            "name": "c",
            "in": "query",
            "required": true,
            "description": "Tenant slug",
            "schema": {
              "type": "string",
              "example": "tv10poc"
            }
          },
          {
            "name": "_HLS_pathway",
            "in": "query",
            "description": "The pathway the player is on now (ignored when malformed)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_HLS_throughput",
            "in": "query",
            "description": "Measured throughput in bit/s (accepted",
            "not used)": null,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sid",
            "in": "query",
            "description": "Playback session id (`[A-Za-z0-9_-]{1,64}`) for sticky pathways and overflow buckets",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The steering manifest",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SteeringManifest"
                },
                "example": {
                  "VERSION": 1,
                  "TTL": 300,
                  "RELOAD-URI": "https://steer.viewstream.co.il/steer?c=tv10poc&sid=k2m9Xq4tR8",
                  "PATHWAY-PRIORITY": [
                    "il",
                    "bunny"
                  ]
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/playback/session": {
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "The playback session issuer used by the hosted player",
        "description": "Public, no credentials; CORS answers any origin (the policy's referrer list is the gate). The player sends the\ntenant slug and one target. The issuer resolves the effective policy, then checks the page origin (the Origin\nheader, else the Referer header; the body's `page_origin` only when the browser sent neither AND the request\ncarries an API key of this tenant as `Authorization: Bearer` — security audit 2026-10-06 M6) against\n`hotlink.referer_allow`, and the caller's country, ASN and\ndatacenter status against `geo`. A refusal is 403 with `X-VS-Deny` and a small JSON body (not problem+json). On\nsuccess it returns the master URL — tokenised only when the policy has `hotlink.token: required` — and the player\nshould ask again `refresh_before_s` before `expires_at`. Limited to about 1 session per second per client IP\n(IPv6 per /48), burst 60. When the tenant has a multi-CDN steering document, the answer adds `pathway`,\n`pathways` and `steering`. Runs here until the Cloudflare Worker at play.viewstream.co.il exists. Body limit 8 KiB.",
        "operationId": "postPlaybackSession",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/PlaybackTarget"
                  },
                  {
                    "type": "object",
                    "required": [
                      "tenant"
                    ],
                    "properties": {
                      "tenant": {
                        "type": "string",
                        "description": "Tenant slug"
                      },
                      "page_origin": {
                        "type": "string",
                        "description": "The embedding page's origin for a server-to-server caller with a tenant API key; ignored when the request has an Origin or Referer header, and for anonymous callers"
                      }
                    }
                  }
                ]
              },
              "example": {
                "tenant": "tv10poc",
                "channel": "main",
                "page_origin": "https://www.example.co.il"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The playback source",
            "headers": {
              "Cache-Control": {
                "description": "`no-store`",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "src": {
                      "type": "string",
                      "format": "uri",
                      "description": "The master URL to play (the first pathway's when steering is on)"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Token expiry; untokenised: now + the policy's ttl_live_s"
                    },
                    "sid": {
                      "type": "string",
                      "description": "Playback session id (ULID)"
                    },
                    "protected": {
                      "type": "boolean",
                      "description": "The src carries a path token"
                    },
                    "refresh_before_s": {
                      "type": "integer",
                      "description": "Ask for a new session this many seconds before expires_at (300)"
                    },
                    "pathway": {
                      "type": "string",
                      "description": "Steering only: the pathway of src"
                    },
                    "pathways": {
                      "type": "array",
                      "description": "Steering only: sources by pathway, in priority order",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "src": {
                            "type": "string",
                            "format": "uri"
                          }
                        }
                      }
                    },
                    "steering": {
                      "type": "object",
                      "description": "Steering only: the HLS content-steering server",
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "ttl": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "src": "https://cdn.tv10poc.vustream.net/t/EXAMPLE-PAYLOAD.EXAMPLE-SIGNATURE/live/tv10poc/main/master.m3u8",
                  "expires_at": "2026-10-06T14:00:00Z",
                  "sid": "01K6WQ8Z3T9VJ4M7N0C5R1XWDA",
                  "protected": true,
                  "refresh_before_s": 300
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "403": {
            "description": "Refused at issue; `X-VS-Deny` = referer, geo, asn or datacenter. `slate: true` = show the regional slate",
            "headers": {
              "X-VS-Deny": {
                "description": "The refusal reason",
                "schema": {
                  "type": "string",
                  "enum": [
                    "referer",
                    "geo",
                    "asn",
                    "datacenter"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reason": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    },
                    "slate": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "reason": "geo",
                  "detail": "country DE is not on the allow list",
                  "slate": true
                }
              }
            }
          },
          "404": {
            "description": "No such tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "No target named, an invalid target, or the channel/asset does not exist",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many sessions from this address (`rate_limited`)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/playback/keys": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The tenant's playback signing key ids, never the keys (scope `keys:manage`)",
        "description": "**Required scope:** `keys:manage`\n\nNewest first. The key without `retire_at` signs new tokens; retired keys still verify tokens until `retire_at`. `exported_at` is set once the key was downloaded for self-signing.",
        "operationId": "listPlaybackKeys",
        "responses": {
          "200": {
            "description": "Signing keys (metadata only)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybackSigningKey"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "kid": "k2",
                      "created_at": "2026-10-05T08:00:00Z",
                      "retire_at": null,
                      "exported_at": null
                    },
                    {
                      "kid": "k1",
                      "created_at": "2026-09-28T11:20:00Z",
                      "retire_at": "2026-10-06T08:00:00Z",
                      "exported_at": "2026-09-29T07:45:10Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "keys:manage"
      }
    },
    "/v1/playback/keys/rotate": {
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Rotate the playback signing key — a new kid signs, older keys stay valid for 24 h (scope `keys:manage`)",
        "description": "**Required scope:** `keys:manage`\n\nCreates the next key (`k<n+1>`), which signs from now on; every other active key gets `retire_at` = now + 24 h, so tokens already issued keep working. The edges receive the new key with the next policy map (≈ 10 s). No body. Audits `playback_key.rotate`.",
        "operationId": "rotatePlaybackKeys",
        "responses": {
          "200": {
            "description": "The keys after rotation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlaybackSigningKey"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "kid": "k2",
                      "created_at": "2026-10-06T08:00:00Z",
                      "retire_at": null,
                      "exported_at": null
                    },
                    {
                      "kid": "k1",
                      "created_at": "2026-09-28T11:20:00Z",
                      "retire_at": "2026-10-07T08:00:00Z",
                      "exported_at": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "keys:manage"
      }
    },
    "/v1/channels/{id}/blackouts": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The channel's geo blackout windows",
        "description": "**Required scope:** `channels:read`\n\nAll blackout windows of the channel, past and future, ordered by start time.",
        "operationId": "listBlackouts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Blackouts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProtectionBlackout"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0199b0a1-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
                      "channel_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                      "programme_id": "0199b09f-77aa-7b12-9c34-56d78e90fa12",
                      "title": "ליגת העל — מכבי חיפה נגד הפועל באר שבע",
                      "start_at": "2026-10-11T17:45:00Z",
                      "end_at": "2026-10-11T19:50:00Z",
                      "geo": {
                        "mode": "allow",
                        "countries": [
                          "IL"
                        ]
                      },
                      "action": "slate",
                      "created_by": "0199a3c4-2b30-7d40-9e5f-6a7b8c9d0e1f",
                      "created_at": "2026-10-06T10:02:13Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      },
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Create a blackout — during the window (and permanently for overlapping catch-up, recordings and clips)…",
        "description": "**Required scope:** `delivery:write`\n\nCreate a blackout — during the window (and permanently for overlapping catch-up, recordings and clips) out-of-territory viewers get the slate\n\nA rights window on the channel: viewers outside the territory (`geo.mode: allow` = only these countries, `deny` =\nall but these) get the regional slate (`action: slate`, default) or 403 during the window — and permanently for\ncatch-up, recordings and clips that overlap it. With `programme_id` (a programme of this channel) the missing\n`start_at`/`end_at`/`title` come from the programme. The window must end after it starts and last at most 7 days.\nReaches the edges with the policy map (≈ 10 s). Audits `blackout.create`; emits `policy.changed` (`change: blackout.create`).",
        "operationId": "createBlackout",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "start_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Required unless programme_id gives it"
                  },
                  "end_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Required unless programme_id gives it; after start_at, at most 7 days later"
                  },
                  "programme_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "A programme of this channel (fills the window and title)"
                  },
                  "title": {
                    "type": "string"
                  },
                  "geo": {
                    "type": "object",
                    "required": [
                      "countries"
                    ],
                    "properties": {
                      "mode": {
                        "type": "string",
                        "enum": [
                          "allow",
                          "deny"
                        ],
                        "default": "allow"
                      },
                      "countries": {
                        "type": "array",
                        "minItems": 1,
                        "items": {
                          "type": "string",
                          "pattern": "^[A-Z]{2}$"
                        },
                        "description": "ISO 3166-1 alpha-2 (the rights territory)"
                      }
                    }
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "slate",
                      "403"
                    ],
                    "default": "slate"
                  }
                }
              },
              "example": {
                "programme_id": "0199b09f-77aa-7b12-9c34-56d78e90fa12",
                "geo": {
                  "mode": "allow",
                  "countries": [
                    "IL"
                  ]
                },
                "action": "slate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The blackout",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProtectionBlackout"
                },
                "example": {
                  "id": "0199b0a1-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
                  "channel_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                  "programme_id": "0199b09f-77aa-7b12-9c34-56d78e90fa12",
                  "title": "ליגת העל — מכבי חיפה נגד הפועל באר שבע",
                  "start_at": "2026-10-11T17:45:00Z",
                  "end_at": "2026-10-11T19:50:00Z",
                  "geo": {
                    "mode": "allow",
                    "countries": [
                      "IL"
                    ]
                  },
                  "action": "slate",
                  "created_by": "0199a3c4-2b30-7d40-9e5f-6a7b8c9d0e1f",
                  "created_at": "2026-10-06T10:02:13Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/channels/{id}/blackouts/{bid}": {
      "delete": {
        "tags": [
          "protection"
        ],
        "summary": "Delete a blackout",
        "description": "**Required scope:** `delivery:write`\n\nRemoves the window; the edges stop applying it with the next policy map (≈ 10 s). Audits `blackout.delete`; emits `policy.changed` (`change: blackout.delete`).",
        "operationId": "deleteBlackout",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Channel id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "bid",
            "in": "path",
            "required": true,
            "description": "Blackout id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/security/leaks": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "Suspected leaks found by the leak job",
        "description": "**Required scope:** `stats:read`\n\nSuspected leaks found by the leak job — a sid seen from > N networks, > M ASNs or > X× one viewer's bytes in 10 min\n\nUp to 200 suspected leaked playback sessions, most recently seen first, with the tenant's current thresholds (`settings`). Viewer IPs appear only as hashes.",
        "operationId": "listLeaks",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only leaks in this state (default all)",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "revoked",
                "dismissed"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leaks and the thresholds",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProtectionLeak"
                      }
                    },
                    "settings": {
                      "$ref": "#/components/schemas/ProtectionSettings"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f",
                      "sid": "01K6WQ8Z3T9VJ4M7N0C5R1XWDA",
                      "status": "open",
                      "reasons": [
                        "prefixes",
                        "asns"
                      ],
                      "prefixes": 5,
                      "asns": 3,
                      "bytes": 2147483648,
                      "requests": 5120,
                      "ip_hashes": [
                        "3f9a1c2b7d4e",
                        "8b0e6d5a1f2c"
                      ],
                      "asn_list": [
                        12400,
                        1680,
                        8551
                      ],
                      "countries": [
                        "IL"
                      ],
                      "user_agents": [
                        "Mozilla/5.0 (SMART-TV; Linux; Tizen 7.0)"
                      ],
                      "paths": [
                        "/live/tv10poc/main/"
                      ],
                      "first_seen": "2026-10-06T07:40:00Z",
                      "last_seen": "2026-10-06T07:50:00Z",
                      "created_at": "2026-10-06T07:51:02Z",
                      "updated_at": "2026-10-06T07:51:02Z"
                    }
                  ],
                  "settings": {
                    "leak_prefixes": 3,
                    "leak_asns": 2,
                    "leak_bytes_x": 3,
                    "top_rung_kbps": 5000,
                    "auto_revoke": false,
                    "updated_at": "0001-01-01T00:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "status is not open, revoked or dismissed (validation_error)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/security/leaks/{id}/dismiss": {
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Dismiss a suspected leak (a legitimate multi-network viewer)",
        "description": "**Required scope:** `security:manage`\n\nMarks the leak `dismissed`; the session is not revoked. No body. Audits `leak.dismiss`.",
        "operationId": "dismissLeak",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Leak id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Dismissed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage"
      }
    },
    "/v1/security/revocations": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The tenant's revoked playback sessions",
        "description": "**Required scope:** `stats:read`\n\nThe 200 most recent revocations, newest first, including expired ones (`expires_at` in the past = no longer enforced).",
        "operationId": "listRevocations",
        "responses": {
          "200": {
            "description": "Revocations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProtectionRevocation"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "sid": "01K6WQ8Z3T9VJ4M7N0C5R1XWDA",
                      "customer_id": "0199a3c4-1a20-7c10-8d4e-5b6c7d8e9f01",
                      "reason": "shared on a piracy site",
                      "created_by": "key:ab12cd34",
                      "leak_id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f",
                      "expires_at": "2026-10-06T20:05:00Z",
                      "created_at": "2026-10-06T08:05:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Revoke a playback session — the edges refuse its tokens within 10 s",
        "description": "**Required scope:** `security:manage`\n\nRevokes a playback session id: the edges refuse every token of that sid (`X-VS-Deny: revoked`) from the next\npolicy map (≈ 10 s) until `expires_at`. Name the session by `sid`, or by `leak_id` (a leak of this tenant; its\nsid is used and the leak becomes `revoked`). `ttl_s` 1–604800, default (and for any value outside the range)\n43200 (12 h — longer than a live token). Audits `session.revoke`; emits the `security.revoked` event.",
        "operationId": "postRevocation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sid": {
                    "type": "string",
                    "pattern": "^[0-9A-Za-z_-]{8,64}$",
                    "description": "Playback session id (required unless leak_id is given)"
                  },
                  "leak_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "A suspected leak of this tenant; an unknown id or one of another tenant is ignored"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "ttl_s": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 604800,
                    "default": 43200
                  }
                }
              },
              "example": {
                "leak_id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f",
                "reason": "shared on a piracy site"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The revocation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProtectionRevocation"
                },
                "example": {
                  "sid": "01K6WQ8Z3T9VJ4M7N0C5R1XWDA",
                  "customer_id": "0199a3c4-1a20-7c10-8d4e-5b6c7d8e9f01",
                  "reason": "shared on a piracy site",
                  "created_by": "key:ab12cd34",
                  "leak_id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f",
                  "expires_at": "2026-10-06T20:05:00Z",
                  "created_at": "2026-10-06T08:05:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage"
      }
    },
    "/v1/security/settings": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "Leak thresholds and auto-revoke (defaults N = 3 networks, M = 2 ASNs, X = 3, top rung 5000 kbps, auto-revoke off)",
        "description": "**Required scope:** `stats:read`\n\nThe tenant's leak-detection settings; a tenant that never saved any gets the defaults (`updated_at` is then the zero time).",
        "operationId": "getSecuritySettings",
        "responses": {
          "200": {
            "description": "The settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProtectionSettings"
                },
                "example": {
                  "leak_prefixes": 3,
                  "leak_asns": 2,
                  "leak_bytes_x": 3,
                  "top_rung_kbps": 5000,
                  "auto_revoke": false,
                  "updated_at": "0001-01-01T00:00:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "protection"
        ],
        "summary": "Change leak thresholds / auto-revoke",
        "description": "**Required scope:** `security:manage`\n\nPartial — omitted fields keep their value. Ranges `leak_prefixes` and `leak_asns` 2–100, `leak_bytes_x` 1–1000, `top_rung_kbps` 100–100000. With `auto_revoke` the leak job revokes suspected sessions itself. Audits `security.settings`.",
        "operationId": "putSecuritySettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "leak_prefixes": {
                    "type": "integer",
                    "minimum": 2,
                    "maximum": 100,
                    "description": "N: distinct networks (/24 or /48) per sid in 10 min"
                  },
                  "leak_asns": {
                    "type": "integer",
                    "minimum": 2,
                    "maximum": 100,
                    "description": "M: distinct ASNs per sid in 10 min"
                  },
                  "leak_bytes_x": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 1000,
                    "description": "X: bytes above X × what one viewer of the top rung can pull"
                  },
                  "top_rung_kbps": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 100000,
                    "description": "Bitrate of the top rendition",
                    "for the bytes rule": null
                  },
                  "auto_revoke": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "leak_prefixes": 4,
                "auto_revoke": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProtectionSettings"
                },
                "example": {
                  "leak_prefixes": 4,
                  "leak_asns": 2,
                  "leak_bytes_x": 3,
                  "top_rung_kbps": 5000,
                  "auto_revoke": true,
                  "updated_at": "2026-10-06T08:10:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage"
      }
    },
    "/v1/security/watermark": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "A/B session watermarking — settings, hostnames with the edge mixer switch, assets with variants",
        "description": "**Required scope:** `stats:read`\n\nv3 (ETSI TS 104 002 A/B variant model, in-house PoC embedder). Everything is off by default: no hostname\nmixes, no asset has variants. `settings.rungs` is how many top rungs get variants (1–2), `settings.strength` the\npeak luma offset of the mark in 8-bit levels (1–8, default 3). Variants are made for VOD assets only; mixing\nneeds signed playback links (a token policy) — sessions without a token get the unmarked picture.",
        "operationId": "getWatermark",
        "responses": {
          "200": {
            "description": "Settings, hostnames, assets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "settings": {
                      "type": "object",
                      "properties": {
                        "rungs": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 2
                        },
                        "strength": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 8
                        },
                        "has_key": {
                          "type": "boolean",
                          "description": "The tenant's selection key exists (created on first use; never returned)"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "hosts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "hostname": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "cdn",
                              "distribution"
                            ]
                          },
                          "enabled": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "assets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WatermarkAsset"
                      }
                    },
                    "notice": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "settings": {
                    "rungs": 1,
                    "strength": 3,
                    "has_key": true,
                    "updated_at": "2026-10-08T09:00:00Z"
                  },
                  "hosts": [
                    {
                      "hostname": "cdn.tv10.example",
                      "kind": "cdn",
                      "enabled": true
                    }
                  ],
                  "assets": [
                    {
                      "asset_id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f",
                      "status": "ready",
                      "rungs": [
                        "1080p"
                      ],
                      "segment_ms": 2000,
                      "segments": 1800,
                      "strength": 3,
                      "stale": false,
                      "created_at": "2026-10-08T09:00:00Z",
                      "updated_at": "2026-10-08T09:40:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`feature_disabled`: watermarking is not available on this platform yet",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "protection"
        ],
        "summary": "Change the A/B watermark settings (rungs, strength)",
        "description": "**Required scope:** `security:manage`\n\nCreates the tenant's selection key on first use. Changes apply to variants made afterwards. Audits `watermark.settings`.",
        "operationId": "putWatermark",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rungs": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2
                  },
                  "strength": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 8
                  }
                }
              },
              "example": {
                "rungs": 1,
                "strength": 3
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings",
            "content": {
              "application/json": {
                "example": {
                  "rungs": 1,
                  "strength": 3,
                  "has_key": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage"
      }
    },
    "/v1/security/watermark/hosts/{hostname}": {
      "put": {
        "tags": [
          "protection"
        ],
        "summary": "Switch the A/B mixer on or off for one of the tenant's hostnames",
        "description": "**Required scope:** `security:manage`\n\nThe edges pick the change up with the next policy map (≤ 10 s). Audits `watermark.host`.",
        "operationId": "putWatermarkHost",
        "parameters": [
          {
            "name": "hostname",
            "in": "path",
            "required": true,
            "description": "The tenant CDN hostname or one of its library distributions",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The hostnames",
            "content": {
              "application/json": {
                "example": {
                  "hosts": [
                    {
                      "hostname": "cdn.tv10.example",
                      "kind": "cdn",
                      "enabled": true
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage"
      }
    },
    "/v1/assets/{id}/watermark": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The asset's A/B variants",
        "description": "**Required scope:** `assets:read`\n\n`status`: none (never made), queued, ready, failed or off (made, not mixed). `stale` = re-encoded since: make them again.",
        "operationId": "getAssetWatermark",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The variants",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatermarkAsset"
                },
                "example": {
                  "asset_id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f",
                  "status": "queued",
                  "rungs": [
                    "1080p"
                  ],
                  "stale": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "assets:read"
      },
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Make (or remake) the asset's A/B variants",
        "description": "**Required scope:** `security:manage`\n\nQueues a `watermark_embed` job on the transcode workers: the top rung(s) are encoded twice from the master with the\nmark (+ and −) and stored next to the renditions (`<rendition prefix>wm/a|b/<rung>/`). Takes about 2 × the\nencode time of those rungs. Only ready, clear (not AES-128 encrypted) assets. Audits `watermark.embed`.",
        "operationId": "postAssetWatermark",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatermarkAsset"
                },
                "example": {
                  "asset_id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f",
                  "status": "queued",
                  "rungs": [
                    "1080p"
                  ],
                  "stale": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage"
      },
      "patch": {
        "tags": [
          "protection"
        ],
        "summary": "Stop or resume mixing the asset's variants",
        "operationId": "patchAssetWatermark",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The variants",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatermarkAsset"
                },
                "example": {
                  "asset_id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f",
                  "status": "off",
                  "rungs": [
                    "1080p"
                  ],
                  "stale": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage",
        "description": "**Required scope:** `security:manage`"
      }
    },
    "/v1/security/watermark/detections": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "Traces of leaked recordings (newest first)",
        "operationId": "listWatermarkDetections",
        "parameters": [
          {
            "name": "leak_id",
            "in": "query",
            "required": false,
            "description": "Only the detections of one leak case",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Detections",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WatermarkDetection"
                      }
                    }
                  }
                },
                "example": {
                  "items": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read",
        "description": "**Required scope:** `stats:read`"
      },
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Trace a leaked recording of a watermarked asset back to its playback session",
        "description": "**Required scope:** `security:manage`\n\nWith `sample_url` the detector starts at once; without it, PUT the file to the returned `upload.url`. The detector\naligns the recording to the asset, decides A or B for every segment it fully covers, and the control plane scores\nevery session that fetched the asset between `since` and `until` (default the last 7 days, at most 60). A\nsession with confidence ≥ 0.999 opens (or joins) a leak case with reason `watermark`. Audits `watermark.detect`.",
        "operationId": "postWatermarkDetection",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "asset_id"
                ],
                "properties": {
                  "asset_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "sample_url": {
                    "type": "string",
                    "description": "An http(s) URL of the recording"
                  },
                  "since": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "until": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              "example": {
                "asset_id": "0199b2d0-11aa-7c3e-8f20-3a4b5c6d7e8f"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "example": {
                  "detection": {
                    "id": "0199b2d0-22bb-7c3e-8f20-3a4b5c6d7e8f",
                    "status": "awaiting_sample"
                  },
                  "upload": {
                    "method": "PUT",
                    "url": "/v1/security/watermark/detections/0199b2d0-22bb-7c3e-8f20-3a4b5c6d7e8f/sample",
                    "max_bytes": 2147483648
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage"
      }
    },
    "/v1/security/watermark/detections/{id}": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "One trace of a leaked recording, with the scored sessions",
        "operationId": "getWatermarkDetection",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Detection id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The detection",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatermarkDetection"
                },
                "example": {
                  "id": "0199b2d0-22bb-7c3e-8f20-3a4b5c6d7e8f",
                  "status": "done",
                  "top_sid": "01K6WQ8Z3T9VJ4M7N0C5R1XWDA",
                  "confidence": 0.99999
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read",
        "description": "**Required scope:** `stats:read`"
      }
    },
    "/v1/security/watermark/detections/{id}/sample": {
      "put": {
        "tags": [
          "protection"
        ],
        "summary": "Upload the leaked recording of a detection (the raw file as the body)",
        "description": "**Required scope:** `security:manage`\n\nUp to 2 GB, with Content-Length; any container FFmpeg reads. The detector job is queued when the upload ends. Audits `watermark.sample`.",
        "operationId": "putWatermarkSample",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Detection id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "video/mp4": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatermarkDetection"
                },
                "example": {
                  "id": "0199b2d0-22bb-7c3e-8f20-3a4b5c6d7e8f",
                  "status": "queued"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "description": "Empty or over 2 GB (validation_error)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "security:manage"
      }
    },
    "/v1/policies/{id}/drm-impact": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "What changing this policy's drm.mode would re-encode",
        "description": "**Required scope:** `stats:read`\n\nWhat changing this policy's drm.mode would re-encode — ready assets, their duration and an estimate (scope `stats:read`)\n\nRenditions are stored encrypted or clear, never both: changing drm.mode on a policy that\napplies to ready assets creates a re-encode job for each. Returns how many assets would change to `mode`, their\ntotal duration and a rough wall-clock estimate (two worker slots at about 2× real time). `supported` = the mode\ncan be used today (`multi` cannot). Read-only; Studio shows it before the change.",
        "operationId": "getPolicyDRMImpact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Policy id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "description": "The drm.mode to evaluate",
            "schema": {
              "type": "string",
              "enum": [
                "none",
                "aes128",
                "multi"
              ],
              "default": "aes128"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Impact",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mode": {
                      "type": "string"
                    },
                    "supported": {
                      "type": "boolean"
                    },
                    "assets": {
                      "type": "integer"
                    },
                    "duration_s": {
                      "type": "integer"
                    },
                    "estimate_s": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "mode": "aes128",
                  "supported": true,
                  "assets": 42,
                  "duration_s": 151200,
                  "estimate_s": 37800
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/drm/settings": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The tenant's DRM service switch, the key-fetch leak flag and the endpoint URLs",
        "description": "**Required scope:** `stats:read`\n\nDRM as a service (migration 0083) is off for every tenant until switched on here. `service_enabled` lets the\ntenant's auth codes call the SPEKE v2 key provider (`POST /drm/speke/v2`) and the licence endpoints;\n`leak_key_datacenter` adds the leak reason `key_datacenter` for playback sessions whose `/k/` key fetches come\nfrom a datacenter or hosting network (the list of the geo block). `endpoints` are the URLs to configure in an\nencoder or DRM proxy. `fairplay_ksm_configured` says whether this platform can issue FairPlay licences yet.",
        "operationId": "getDRMSettings",
        "responses": {
          "200": {
            "description": "Settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DRMSettingsView"
                },
                "example": {
                  "settings": {
                    "service_enabled": false,
                    "leak_key_datacenter": false,
                    "updated_by": null,
                    "updated_at": null
                  },
                  "endpoints": {
                    "speke_v2": "https://api.viewstream.co.il/drm/speke/v2",
                    "fairplay_certificate": "https://api.viewstream.co.il/drm/fairplay/tv10poc/certificate",
                    "fairplay_licence": "https://api.viewstream.co.il/drm/fairplay/tv10poc/licence"
                  },
                  "fairplay_ksm_configured": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "protection"
        ],
        "summary": "Switch the DRM service and the key-fetch leak flag on or off (audited drm.settings)",
        "description": "**Required scope:** `security:manage`\n\nAbsent fields stay as they are. Switching `service_enabled` off makes every auth code of the tenant answer 403\nat once (the codes are kept). Needs `security:manage`; audited as `drm.settings`.",
        "operationId": "putDRMSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "service_enabled": {
                    "type": "boolean"
                  },
                  "leak_key_datacenter": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "service_enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DRMSettingsView"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "security:manage"
      }
    },
    "/v1/drm/auth-codes": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The tenant's named DRM auth codes, newest first (never the secret; revoked ones included)",
        "description": "**Required scope:** `stats:read`\n\nAuth codes are the credential of the SPEKE v2 key provider and the licence endpoints (header `Auth-Code`). Each\nhas a name, the first 8 characters (`prefix`) for recognising it, its scopes (`speke`, `licence`), an optional\nplayback policy whose licence rules apply to its requests, and when it was last used (at most once a minute).",
        "operationId": "listDRMAuthCodes",
        "responses": {
          "200": {
            "description": "Codes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DRMAuthCode"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "Create a named DRM auth code — the code is returned once (audited drm.auth_code.create)",
        "description": "**Required scope:** `keys:manage`\n\n`name` (1–80 characters, unique among the active codes), `scopes` (default both `speke` and `licence`),\n`policy_id` (a playback policy of the tenant whose `drm.licence` applies; default the tenant default policy).\nThe answer carries `code` (`vsdrm_<8>_<40>`) once; only a SHA-256 of it is stored. Needs `keys:manage`.",
        "operationId": "createDRMAuthCode",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "speke",
                        "licence"
                      ]
                    }
                  },
                  "policy_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid"
                  }
                }
              },
              "example": {
                "name": "Harmonic packager",
                "scopes": [
                  "speke"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "auth_code",
                    "code"
                  ],
                  "properties": {
                    "auth_code": {
                      "$ref": "#/components/schemas/DRMAuthCode"
                    },
                    "code": {
                      "type": "string"
                    },
                    "notice": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "An active code with this name exists (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "keys:manage"
      }
    },
    "/v1/drm/auth-codes/{id}": {
      "delete": {
        "tags": [
          "protection"
        ],
        "summary": "Revoke a DRM auth code (audited drm.auth_code.revoke; idempotent)",
        "description": "**Required scope:** `keys:manage`\n\nThe code stops working at once and stays listed with `revoked_at`. Needs `keys:manage`.",
        "operationId": "revokeDRMAuthCode",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Auth code id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DRMAuthCode"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "keys:manage"
      }
    },
    "/v1/drm/stats": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "DRM statistics — licences, keys and certificate fetches by system, request type, status, platform…",
        "description": "**Required scope:** `stats:read`\n\nDRM statistics — licences, keys and certificate fetches by system, request type, status, platform, security level and day; error codes; alerts\n\nRead from the same hourly counters as the usage meters `drm_licences_k` and `drm_keys`, so the counts match the\nbill. Default the last 7 days (`days` 1–92, or `from`/`to` RFC 3339, at most 93 days); days in the tenant's time\nzone. `alerts` lists device platforms whose licence error rate exceeded 2 % in the last full hour. Errors are\noutcomes `error` and `denied`.",
        "operationId": "getDRMStats",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 92,
              "default": 7
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Statistics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DRMStats"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/drm/fairplay": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The tenant's FairPlay Streaming credentials — status only, never the key or the ASk",
        "description": "**Required scope:** `stats:read`\n\nFairPlay production credentials are issued by Apple to the streaming company itself (the tenant). `configured`\n= a certificate, its private key and the Application Secret key (ASk) are stored and consistent;\n`ksm_configured` = the platform has the key module built from Apple's FPS Server SDK. Both are needed before\n`POST /drm/fairplay/{tenant}/licence` issues licences.",
        "operationId": "getFairPlay",
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FairPlayStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "protection"
        ],
        "summary": "Store the tenant's FairPlay credentials (sealed; audited drm.fairplay.put)",
        "description": "**Required scope:** `security:manage`\n\n`certificate_pem` (the FPS certificate from Apple, PEM or base64 DER), `private_key_pem` (the RSA key the CSR\nwas made with, PKCS#1 or PKCS#8) and `ask_hex` (the 16-byte Application Secret key, hex). Checked (the key must\nbelong to the certificate) and stored sealed; never returned. Needs `security:manage`.",
        "operationId": "putFairPlay",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "certificate_pem",
                  "private_key_pem",
                  "ask_hex"
                ],
                "properties": {
                  "certificate_pem": {
                    "type": "string"
                  },
                  "private_key_pem": {
                    "type": "string"
                  },
                  "ask_hex": {
                    "type": "string",
                    "pattern": "^[0-9a-fA-F]{32}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FairPlayStatus"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "security:manage"
      },
      "delete": {
        "tags": [
          "protection"
        ],
        "summary": "Remove the tenant's FairPlay credentials (audited drm.fairplay.delete)",
        "operationId": "deleteFairPlay",
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Not available yet (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "security:manage",
        "description": "**Required scope:** `security:manage`"
      }
    },
    "/drm/speke/v2": {
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "SPEKE v2.0 key provider",
        "description": "SPEKE v2.0 key provider — CPIX 2.3 key request from an encoder or packager, answered with the keys and DRM signalling\n\nFor tenants with the DRM service on. The credential is a DRM auth code with scope `speke`, in the `Auth-Code`\nheader (also accepted: `X-Auth-Code`, `Authorization: Bearer`, or `?auth_code=` for encoders that only take a URL\n— the query may end up in proxy logs). The body is a SPEKE v2 CPIX 2.3 document: the content keys the encryptor\nwants (`kid`, `commonEncryptionScheme` cenc or cbcs), the DRM systems (Widevine, PlayReady, FairPlay) and the\nsignalling it needs per system (`PSSH`, `ContentProtectionData`, `HLSSignalingData` media/master,\n`SmoothStreamingProtectionHeaderData`), optional key periods and usage rules (`intendedTrackType`, filters).\nKeys are generated on first request (stored sealed, never logged) and the same kid always returns the same key;\na kid of another tenant answers 409. The answer is the same document with `Data/Secret/PlainValue` per key and\nthe signalling filled; periods and usage rules are echoed. Header `X-Speke-Version: 2.0`. Every key in every\nanswer counts in the `drm_keys` meter. FairPlay uses `skd://<kid>` as its key URI.",
        "operationId": "postSPEKE",
        "parameters": [
          {
            "name": "Auth-Code",
            "in": "header",
            "required": false,
            "description": "DRM auth code (scope speke)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "auth_code",
            "in": "query",
            "required": false,
            "description": "DRM auth code, for encoders that cannot send headers",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "CPIX 2.3 document (SPEKE v2.0 request), at most 1 MiB"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "CPIX 2.3 document with the keys and signalling",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Not a valid SPEKE v2 CPIX request (`validation_error`; e.g. an unsupported DRM system or FairPlay with cenc)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked auth code",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The DRM service is off for the tenant, or the code lacks scope speke",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "A requested kid belongs to another tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "Body larger than 1 MiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many invalid auth codes from this address",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Not available (`feature_disabled`, migration 0083 missing)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/drm/fairplay/{tenant}/certificate": {
      "get": {
        "tags": [
          "protection"
        ],
        "summary": "The tenant's FairPlay application certificate (DER) for the player to build its SPC",
        "description": "Public data (the certificate, not its key). 404 unless the tenant has the DRM service on and FairPlay\ncredentials stored. Counted as a `certificate` request in the DRM statistics.",
        "operationId": "getFairPlayCertificate",
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "description": "Tenant slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The certificate (DER)",
            "content": {
              "application/x-x509-ca-cert": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No certificate (`fairplay_not_configured`, or plain 404)"
          }
        },
        "security": []
      }
    },
    "/drm/fairplay/{tenant}/licence": {
      "post": {
        "tags": [
          "protection"
        ],
        "summary": "FairPlay licence — SPC in, CKC out (answers 503 fairplay_not_configured until Apple's key module and credentials exist)",
        "description": "For a tenant's DRM proxy (credential: auth code with scope `licence` in `Auth-Code`). `kid` is the key id (the\nhost part of `skd://<kid>`). The body is the SPC — raw, base64, or JSON `{\"spc\": \"<base64>\"}` (then the answer is\nJSON `{\"ckc\": \"<base64>\"}`, else the raw CKC). The licence carries the policy's `drm.licence` rules (HDCP type,\nlease, rental and playback durations, persistence). Issuing CKCs needs Apple's FPS Server SDK key module\n(`FAIRPLAY_KSM_URL`) and the tenant's Apple credentials (`PUT /v1/drm/fairplay`); without them the answer is 503\n`fairplay_not_configured`. Every answer counts in the DRM statistics; issued licences in `drm_licences_k`.",
        "operationId": "postFairPlayLicence",
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "description": "Tenant slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kid",
            "in": "query",
            "required": true,
            "description": "Key id (UUID; skd:// prefix tolerated)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Auth-Code",
            "in": "header",
            "required": true,
            "description": "DRM auth code (scope licence)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "spc": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The CKC",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ckc": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No SPC or a malformed kid",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked auth code",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Service off, scope missing, or the code belongs to another tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such key for this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "SPC larger than 64 KiB",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many invalid auth codes from this address",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "The key module failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "`fairplay_not_configured` (no Apple credentials or no key module), or `feature_disabled`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/distributions/{id}/sign": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Sign a URL for a token distribution (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nSigns `path` (absolute, no `..`) or `url` (on this distribution's hostname; wins over `path`) for `expires_in_s`\n(60–604800, default 3600). HMAC: `https://<host>/<exp>/<sig><path>`, sig = base64url(HMAC-SHA256(secret, exp || scope));\n`scope: dir` (default) signs the directory of the path so HLS relative URLs (variants, segments) verify too; `exact`\nsigns the path only. JWT: `?t=<HS256 {exp, sub: path or directory prefix}>`. The query string is kept. 409 when the\ndistribution's token mode is `none`. Nothing is stored.",
        "operationId": "signDistributionURL",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "path": {
                    "type": "string",
                    "description": "Absolute path on the distribution host"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Full URL on the distribution host (instead of path)"
                  },
                  "expires_in_s": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 604800,
                    "default": 3600
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "dir",
                      "exact"
                    ],
                    "default": "dir"
                  }
                }
              },
              "example": {
                "path": "/live/news/master.m3u8",
                "expires_in_s": 3600
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "token_mode": {
                      "type": "string",
                      "enum": [
                        "hmac",
                        "jwt"
                      ]
                    }
                  }
                },
                "example": {
                  "url": "https://video.example.co.il/1791277200/Xh3k9QpL2mN8vR4tY6wZ1aB5cD7eF0gH/live/news/master.m3u8",
                  "expires_at": "2026-10-06T09:00:00Z",
                  "token_mode": "hmac"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Token mode is `none`; URLs need no signature (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Bad url/path, expires_in_s or scope (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The secret could not be opened",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}/status": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "DNS and TLS status of a distribution hostname on every edge (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nLive check (≤ 8 s): resolves the hostname (IPv4) and tells whether it points at an edge address, then opens TLS to\nevery edge with SNI = the hostname and verifies the certificate chain and name against the system roots (issuer,\nexpiry, days left, error). `tls_ok` is true only when every edge passed. Used by Studio onboarding.",
        "operationId": "distributionStatus",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id (a malformed or foreign id answers 404)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dns": {
                      "type": "object",
                      "properties": {
                        "hostname": {
                          "type": "string"
                        },
                        "cname_target": {
                          "type": "string"
                        },
                        "addresses": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "points_at_edge": {
                          "type": "boolean"
                        },
                        "error": {
                          "type": "string",
                          "description": "`does not resolve` when the lookup failed"
                        }
                      }
                    },
                    "tls": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "edge": {
                            "type": "string"
                          },
                          "site": {
                            "type": "string"
                          },
                          "ok": {
                            "type": "boolean"
                          },
                          "issuer": {
                            "type": "string"
                          },
                          "not_after": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "days_left": {
                            "type": "integer"
                          },
                          "error": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "tls_ok": {
                      "type": "boolean"
                    },
                    "enabled": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "dns": {
                    "hostname": "video.example.co.il",
                    "addresses": [
                      "185.37.148.250"
                    ],
                    "points_at_edge": true,
                    "cname_target": "cdn-poc.vustream.net"
                  },
                  "tls": [
                    {
                      "edge": "edge-fornax",
                      "site": "med1",
                      "ok": true,
                      "issuer": "Let's Encrypt R12",
                      "not_after": "2026-12-25T07:12:00Z",
                      "days_left": 80
                    }
                  ],
                  "tls_ok": true,
                  "enabled": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/distributions/{id}/traffic-seen": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Edge requests for the hostname per site in the last `minutes` (scope `stats:read`; onboarding \"traffic seen\")",
        "description": "**Required scope:** `stats:read`\n\nRequests and bytes the edges served for this hostname per site since `minutes` ago (1–1440, default 60), from the\nper-minute traffic rollup. `seen` is true when there was at least one request — Studio uses it to confirm a customer's\nDNS switch. 503 when edge statistics are not configured or not reachable.",
        "operationId": "distributionTrafficSeen",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "minutes",
            "in": "query",
            "description": "Look-back window in minutes",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1440,
              "default": 60
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Traffic",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "hostname": {
                      "type": "string"
                    },
                    "since": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "seen": {
                      "type": "boolean"
                    },
                    "requests": {
                      "type": "integer"
                    },
                    "sites": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "site": {
                            "type": "string"
                          },
                          "requests": {
                            "type": "integer"
                          },
                          "bytes": {
                            "type": "integer"
                          },
                          "last": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "hostname": "video.example.co.il",
                  "since": "2026-10-06T07:00:00Z",
                  "seen": true,
                  "requests": 42,
                  "sites": [
                    {
                      "site": "med1",
                      "requests": 42,
                      "bytes": 1048576,
                      "last": "2026-10-06T07:58:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "minutes out of range (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Edge statistics are not available or not reachable",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/distributions/{id}/certificate": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "The distribution's TLS certificate — managed (Let's Encrypt on the edge) or uploaded (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\n`source: managed` = the edges obtain a Let's Encrypt certificate (HTTP-01), which needs the hostname to resolve to\n`cname_target`. `source: uploaded` = the customer's (or a multi-CDN platform's, e.g. IO River) certificate with its\nmetadata. The private key is never returned. 503 before migration 0092.",
        "operationId": "getDistributionCertificate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Certificate source and metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DistributionCertificate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Uploaded certificates are not available yet (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "delivery"
        ],
        "summary": "Upload the distribution's own TLS certificate (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nUpload the distribution's own TLS certificate (scope `delivery:write`) — for hostnames behind a multi-CDN / IO River\n\nReplaces Let's Encrypt for this hostname. Behind a multi-CDN traffic-steering layer (IO River, NS1, …) the hostname\ndoes not resolve to our edges, so HTTP-01 cannot validate: upload the certificate the platform uses for every\nprovider. Checks: `certificate_pem` (leaf first, then the chain) and `private_key_pem` form a pair; the leaf names\nthe distribution's hostname (wildcards allowed); valid now and for at least 72 h; RSA ≥ 2048, ECDSA or Ed25519.\nThe key is sealed at rest and reaches the edges only through the operator-only distribution export; the edges\nserve it after the next distribution render (minutes). Upload again before expiry (renewal is the uploader's job).\nAudited as `distribution.certificate.upload`.",
        "operationId": "putDistributionCertificate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "certificate_pem",
                  "private_key_pem"
                ],
                "properties": {
                  "certificate_pem": {
                    "type": "string",
                    "description": "PEM: leaf certificate, then intermediates (≤ 64 KiB)"
                  },
                  "private_key_pem": {
                    "type": "string",
                    "description": "PEM private key (PKCS#1, PKCS#8 or SEC1; ≤ 16 KiB)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DistributionCertificate"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution in this tenant (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Not a valid pair for this hostname, or expiring within 72 h (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Uploaded certificates are not available yet (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      },
      "delete": {
        "tags": [
          "delivery"
        ],
        "summary": "Remove the uploaded certificate — back to Let's Encrypt on the edge (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nThe edges return to a managed Let's Encrypt certificate on the next render (needs DNS to point at `cname_target`). Audited as `distribution.certificate.delete`.",
        "operationId": "deleteDistributionCertificate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Distribution id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such distribution, or no uploaded certificate (`not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Uploaded certificates are not available yet (`not_ready`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/delivery/check": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Playback checker — probe a URL through every edge (scope `stats:read`, 12 runs/min/tenant)",
        "description": "**Required scope:** `stats:read`\n\n`url` must be an absolute http(s) URL on one of the tenant's hostnames (probed as https). Each edge is dialled\ndirectly (SNI/Host = the URL host): the URL, then for a master playlist its first variant, then one segment (newest of\na live playlist, first of a VOD one) with `Range: bytes=0-1023`. Per step: status, ms, bytes, X-Cache, the\nAccess-Control-Allow-Origin answered for `origin`, X-VS-Deny and a verdict. `sign: true` signs the URL with the\ndistribution's token first (directory scope, `expires_in_s` default 600, max 86400). The overall `verdict` is the\nfirst non-ok edge verdict. Limited to 12 runs per minute per tenant. Audited as `delivery.check`.",
        "operationId": "deliveryCheck",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "origin": {
                    "type": "string",
                    "description": "Origin header to test CORS with (`https://host[:port]`)"
                  },
                  "sign": {
                    "type": "boolean",
                    "default": false
                  },
                  "expires_in_s": {
                    "type": "integer",
                    "default": 600,
                    "maximum": 86400,
                    "description": "Lifetime of the signature when sign is true"
                  }
                }
              },
              "example": {
                "url": "https://video.example.co.il/live/news/master.m3u8",
                "sign": true,
                "origin": "https://www.example.co.il"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-edge results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "The URL probed (signed when `signed`)"
                    },
                    "signed": {
                      "type": "boolean"
                    },
                    "token_mode": {
                      "type": "string",
                      "enum": [
                        "none",
                        "hmac",
                        "jwt"
                      ]
                    },
                    "verdict": {
                      "type": "string",
                      "description": "ok | token_rejected | geo_blocked | not_found | error | skipped | http_<code>"
                    },
                    "checked_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "edges": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "edge": {
                            "type": "string"
                          },
                          "site": {
                            "type": "string"
                          },
                          "verdict": {
                            "type": "string"
                          },
                          "steps": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/DeliveryProbeStep"
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "url": "https://video.example.co.il/1791277200/Xh3k9QpL2mN8vR4tY6wZ1aB5cD7eF0gH/live/news/master.m3u8",
                  "signed": true,
                  "token_mode": "hmac",
                  "verdict": "ok",
                  "checked_at": "2026-10-06T08:00:00Z",
                  "edges": [
                    {
                      "edge": "edge-fornax",
                      "site": "med1",
                      "verdict": "ok",
                      "steps": [
                        {
                          "step": "master",
                          "url": "https://video.example.co.il/1791277200/Xh3k9QpL2mN8vR4tY6wZ1aB5cD7eF0gH/live/news/master.m3u8",
                          "status": 200,
                          "ms": 12,
                          "bytes": 912,
                          "content_type": "application/vnd.apple.mpegurl",
                          "cache": "MISS",
                          "cors": "https://www.example.co.il",
                          "verdict": "ok"
                        },
                        {
                          "step": "media",
                          "url": "https://video.example.co.il/1791277200/Xh3k9QpL2mN8vR4tY6wZ1aB5cD7eF0gH/live/news/1080p.m3u8",
                          "status": 200,
                          "ms": 9,
                          "bytes": 1460,
                          "cache": "HIT",
                          "verdict": "ok"
                        },
                        {
                          "step": "segment",
                          "url": "https://video.example.co.il/1791277200/Xh3k9QpL2mN8vR4tY6wZ1aB5cD7eF0gH/live/news/1080p/seg_184467.m4s",
                          "status": 206,
                          "ms": 21,
                          "bytes": 1024,
                          "range": "bytes 0-1023/1843210",
                          "cache": "HIT",
                          "verdict": "ok"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeliveryInvalidJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "url not absolute or not on a tenant hostname, or a malformed origin (`validation_error`, fields in `errors[]`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "More than 12 checker runs per minute for this tenant (or the general 20 requests/second limit); `Retry-After` in seconds",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Could not load distributions or sign the URL",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "No edges configured (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/storage/usage": {
      "get": {
        "tags": [
          "storage"
        ],
        "summary": "Storage usage per bucket under the tenant prefix + daily history + channel retention (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nThe latest snapshot of the tenant's prefix in the `vod`, `rec`, `masters` and `ingest` buckets (objects and\nbytes per bucket, `total_bytes`), a daily history of the last 90 days (per UTC day the total of that day's\nlatest snapshot) and, for platform tenants, every channel's recording retention and DVR window. Snapshots come\nfrom the origin's nightly inventory (02:30 IL); when the latest is older than 24 h a read imports a newer\nreport first, and without any report a background listing starts (`refreshing: true` while it runs). A bucket\nwithout a snapshot has `taken_at: null` and zeros. Scope `stats:read`; also available to CDN-only tenants.",
        "operationId": "storageUsage",
        "responses": {
          "200": {
            "description": "Usage",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "prefix",
                    "buckets",
                    "total_bytes",
                    "taken_at",
                    "history",
                    "refreshing",
                    "retention"
                  ],
                  "properties": {
                    "prefix": {
                      "type": "string",
                      "description": "The tenant's key prefix, e.g. `tv10poc/`"
                    },
                    "buckets": {
                      "type": "array",
                      "description": "Always the four buckets vod, rec, masters, ingest in this order",
                      "items": {
                        "type": "object",
                        "required": [
                          "bucket",
                          "objects",
                          "bytes",
                          "taken_at"
                        ],
                        "properties": {
                          "bucket": {
                            "type": "string",
                            "enum": [
                              "vod",
                              "rec",
                              "masters",
                              "ingest"
                            ]
                          },
                          "objects": {
                            "type": "integer",
                            "format": "int64"
                          },
                          "bytes": {
                            "type": "integer",
                            "format": "int64"
                          },
                          "taken_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total_bytes": {
                      "type": "integer",
                      "format": "int64",
                      "description": "Sum of the buckets' latest snapshots"
                    },
                    "taken_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "The newest bucket snapshot"
                    },
                    "history": {
                      "type": "array",
                      "description": "One point per UTC day, oldest first, last 90 days",
                      "items": {
                        "type": "object",
                        "required": [
                          "date",
                          "bytes"
                        ],
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "bytes": {
                            "type": "integer",
                            "format": "int64"
                          }
                        }
                      }
                    },
                    "refreshing": {
                      "type": "boolean",
                      "description": "A background listing is running for this tenant"
                    },
                    "retention": {
                      "type": "array",
                      "description": "Platform tenants only (empty for CDN-only tenants)",
                      "items": {
                        "type": "object",
                        "required": [
                          "channel",
                          "title",
                          "retention_days",
                          "dvr_window_s"
                        ],
                        "properties": {
                          "channel": {
                            "type": "string",
                            "description": "channel slug"
                          },
                          "title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "retention_days": {
                            "type": "integer"
                          },
                          "dvr_window_s": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "prefix": "tv10poc/",
                  "total_bytes": 305645924137,
                  "taken_at": "2026-10-05T23:30:00Z",
                  "refreshing": false,
                  "buckets": [
                    {
                      "bucket": "vod",
                      "objects": 8426,
                      "bytes": 3026262964,
                      "taken_at": "2026-10-05T23:30:00Z"
                    },
                    {
                      "bucket": "rec",
                      "objects": 857598,
                      "bytes": 301311101952,
                      "taken_at": "2026-10-05T23:30:00Z"
                    },
                    {
                      "bucket": "masters",
                      "objects": 125,
                      "bytes": 290128934,
                      "taken_at": "2026-10-05T23:30:00Z"
                    },
                    {
                      "bucket": "ingest",
                      "objects": 5,
                      "bytes": 1018430287,
                      "taken_at": "2026-10-05T23:30:00Z"
                    }
                  ],
                  "history": [
                    {
                      "date": "2026-10-04",
                      "bytes": 301020311552
                    },
                    {
                      "date": "2026-10-05",
                      "bytes": 305645924137
                    }
                  ],
                  "retention": [
                    {
                      "channel": "main",
                      "title": "TV10 main",
                      "retention_days": 7,
                      "dvr_window_s": 7200
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/storage/usage/refresh": {
      "post": {
        "tags": [
          "storage"
        ],
        "summary": "Take a storage usage snapshot now, in the background (scope `storage:manage`)",
        "description": "**Required scope:** `storage:manage`\n\nRe-reads the origin's nightly inventory and stores it when it is newer than the latest snapshot (instant);\nwhen the origin has no report it starts a background listing of the tenant's buckets (at most one per tenant;\nup to 15 minutes). `refreshing` says whether a listing is running — false when the report was imported\ndirectly. Re-read `GET /v1/storage/usage` afterwards. No body. Scope `storage:manage`; also for CDN-only\ntenants. Audited as `storage.usage_refresh`.",
        "operationId": "refreshStorageUsage",
        "responses": {
          "202": {
            "description": "Refresh done or running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "refreshing"
                  ],
                  "properties": {
                    "refreshing": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "refreshing": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Object storage is not configured (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "storage:manage"
      }
    },
    "/v1/storage/keys": {
      "get": {
        "tags": [
          "storage"
        ],
        "summary": "Tenant S3 access-key capability (scope `stats:read`) — not offered at the PoC, with the reason",
        "description": "**Required scope:** `stats:read`\n\nStatic capability answer: per-tenant S3 keys are not offered at the PoC (the object store runs one root account\nand tenants share buckets under their prefix), with the tenant's prefix and the supported alternatives. Read\nonly. Scope `stats:read`; also for CDN-only tenants.",
        "operationId": "storageKeys",
        "responses": {
          "200": {
            "description": "Capability",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "supported",
                    "reason",
                    "prefix",
                    "alternatives"
                  ],
                  "properties": {
                    "supported": {
                      "type": "boolean",
                      "description": "always false at the PoC"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "prefix": {
                      "type": "string"
                    },
                    "alternatives": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "supported": false,
                  "reason": "the PoC object store (versitygw 1.8) runs with a single root account and tenants share buckets under their prefix; per-tenant keys need gateway IAM plus prefix-scoped bucket policies, which are not enabled yet",
                  "prefix": "tv10poc/",
                  "alternatives": [
                    "uploads: POST /v1/uploads (resumable, presigned parts)",
                    "downloads: signed edge URLs of the tenant's distributions",
                    "bulk export: ask Interhost for a one-off copy"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/webhooks": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "summary": "Outbound webhook endpoints of the tenant (scope `webhooks:manage`)",
        "description": "**Required scope:** `webhooks:manage`\n\nLists every outbound webhook endpoint of the tenant (oldest first) with a `secret_hint` (the first 10 characters\nof the signing secret — the full secret is shown only once, at creation), plus `event_types`: the closed list of\nevent types an endpoint may subscribe to. Needs scope `webhooks:manage` (Engineer and up); also available to\nCDN-only tenants.",
        "operationId": "listWebhooks",
        "responses": {
          "200": {
            "description": "Endpoints and the subscribable event types",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "event_types"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookEndpoint"
                      }
                    },
                    "event_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Event types an endpoint may subscribe to (besides `*`)"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01928f3a-6b1c-7d2e-8f40-5a6b7c8d9e0f",
                      "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                      "url": "https://cms.example.co.il/hooks/viewstream",
                      "events": [
                        "asset.ready",
                        "asset.failed",
                        "clip.ready"
                      ],
                      "enabled": true,
                      "created_at": "2026-09-28T09:12:44Z",
                      "secret_hint": "whsec_0123…"
                    }
                  ],
                  "event_types": [
                    "asset.ready",
                    "asset.published",
                    "asset.failed",
                    "clip.ready",
                    "clip.final",
                    "channel.feed_changed",
                    "channel.ingest_failover",
                    "channel.recording_status",
                    "channel.programme_started",
                    "prewarm.finished",
                    "security.leak_suspected",
                    "security.revoked",
                    "policy.changed",
                    "epg.published",
                    "epg.delivery_failed",
                    "alert.firing",
                    "alert.resolved",
                    "artifact.published",
                    "ping"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "webhooks:manage"
      },
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Register an endpoint; the signing secret is returned once",
        "description": "**Required scope:** `webhooks:manage`\n\nRegisters an HTTPS endpoint for the listed event types (or `*`). Each event becomes one `POST` with the JSON\nenvelope `{id, type, at, customer_id, data}` (see `WebhookEnvelope`; `id` is the event id — de-duplicate on it)\nand headers `X-VS-Event`, `X-VS-Delivery` (delivery id), `X-VS-Timestamp` (unix seconds),\n`X-VS-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + \".\" + raw body)>`, `User-Agent: ViewStream-Webhooks/1`.\nAny 2xx within 10 s acknowledges; redirects are not followed. Otherwise retries after 10 s, 1 min, 5 min, 30 min,\n2 h, 12 h (7 attempts), then the delivery is `dead` (redeliver it by hand). The URL must be `https://`, without\ncredentials, on port 443, 80 or 8443, and resolve to a public address (SSRF guard, re-checked on every attempt).\nThe secret (`whsec_` + 64 hex) is in this response only. Audited as `webhook.create`.",
        "operationId": "createWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Absolute https URL (public address; ports 443, 80, 8443; no user:password)"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    },
                    "description": "Event types from `event_types` of the list response, or `*` for all"
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true,
                    "description": "false = registered but nothing is queued for it"
                  }
                }
              },
              "example": {
                "url": "https://cms.example.co.il/hooks/viewstream",
                "events": [
                  "asset.ready",
                  "asset.failed",
                  "clip.ready"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — `secret` appears only in this response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                },
                "example": {
                  "id": "01928f3a-6b1c-7d2e-8f40-5a6b7c8d9e0f",
                  "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                  "url": "https://cms.example.co.il/hooks/viewstream",
                  "events": [
                    "asset.ready",
                    "asset.failed",
                    "clip.ready"
                  ],
                  "enabled": true,
                  "created_at": "2026-09-28T09:12:44Z",
                  "secret_hint": "whsec_0123…",
                  "secret": "whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntegrationBadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "patch": {
        "tags": [
          "webhooks"
        ],
        "summary": "Change url, events or enabled",
        "description": "**Required scope:** `webhooks:manage`\n\nPartial update: only the fields sent change (the secret never changes — delete and re-create the endpoint to\nrotate it). A new `url` passes the same SSRF guard as on create; `events` is validated against the event types.\nDisabling stops new deliveries from being queued; deliveries already queued are then recorded as `dead`\n(\"endpoint disabled\"). Audited as `webhook.update`.",
        "operationId": "patchWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Absolute https URL (same rules as on create)"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    },
                    "description": "Event types or `*`; replaces the list"
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated endpoint (no `secret`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                },
                "example": {
                  "id": "01928f3a-6b1c-7d2e-8f40-5a6b7c8d9e0f",
                  "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                  "url": "https://cms.example.co.il/hooks/viewstream",
                  "events": [
                    "asset.ready",
                    "asset.failed",
                    "clip.ready"
                  ],
                  "enabled": false,
                  "created_at": "2026-09-28T09:12:44Z",
                  "secret_hint": "whsec_0123…"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntegrationBadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "webhooks:manage"
      },
      "delete": {
        "tags": [
          "webhooks"
        ],
        "summary": "Delete an endpoint and its delivery log",
        "description": "**Required scope:** `webhooks:manage`\n\nDeletes the endpoint; its queued deliveries and delivery log go with it. Audited as `webhook.delete`.",
        "operationId": "deleteWebhook",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/webhooks/{id}/test": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Queue a signed `ping` delivery to the endpoint",
        "description": "**Required scope:** `webhooks:manage`\n\nQueues one `ping` event (`data: {endpoint_id, message: \"ViewStream webhook test\"}`) for this endpoint, whatever\nits subscribed events, signed and retried like any other delivery. Answers 202 with the queued delivery; the\ndelivery loop sends it within a few seconds — poll `GET /v1/webhooks/{id}/deliveries` for the outcome. A\ndisabled endpoint records the ping as `dead`.",
        "operationId": "testWebhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook endpoint id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Delivery queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                },
                "example": {
                  "id": "01929b40-11aa-7b2c-9d3e-4f5061728394",
                  "endpoint_id": "01928f3a-6b1c-7d2e-8f40-5a6b7c8d9e0f",
                  "event_type": "ping",
                  "event_id": "01929b40-11a9-7e1f-8a2b-3c4d5e6f7081",
                  "payload": {
                    "id": "01929b40-11a9-7e1f-8a2b-3c4d5e6f7081",
                    "type": "ping",
                    "at": "2026-10-06T07:30:00Z",
                    "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                    "data": {
                      "endpoint_id": "01928f3a-6b1c-7d2e-8f40-5a6b7c8d9e0f",
                      "message": "ViewStream webhook test"
                    }
                  },
                  "status": "pending",
                  "attempts": 0,
                  "next_attempt_at": "2026-10-06T07:30:00Z",
                  "last_status_code": null,
                  "last_error": null,
                  "response_ms": null,
                  "created_at": "2026-10-06T07:30:00Z",
                  "delivered_at": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/webhooks/{id}/deliveries": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "summary": "Delivery log of an endpoint, newest first",
        "description": "**Required scope:** `webhooks:manage`\n\nThe endpoint's deliveries, newest first, with the signed payload, attempt count, last HTTP status/error and the\nnext scheduled attempt. No cursor: `limit` (default 50, at most 200; other values fall back to 50) bounds the\npage.",
        "operationId": "listWebhookDeliveries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook endpoint id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Rows to return; outside 1–200 (or absent) = 50",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01929a77-0c3d-7e4f-8a5b-6c7d8e9f0a1b",
                      "endpoint_id": "01928f3a-6b1c-7d2e-8f40-5a6b7c8d9e0f",
                      "event_type": "asset.ready",
                      "event_id": "01929a77-0c2e-7f10-9a2b-3c4d5e6f7a8b",
                      "payload": {
                        "id": "01929a77-0c2e-7f10-9a2b-3c4d5e6f7a8b",
                        "type": "asset.ready",
                        "at": "2026-10-05T18:02:11Z",
                        "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                        "data": {
                          "asset_id": "01929a51-7d3e-7a10-b2c3-d4e5f6a7b8c9",
                          "external_id": "tv10-2026-1005-07",
                          "title": "מהדורה מרכזית",
                          "duration_ms": 1745320,
                          "ladder": "default",
                          "playback": {
                            "hls": "https://cdn.tv10poc.vustream.net/v/01929a51-7d3e-7a10-b2c3-d4e5f6a7b8c9/master.m3u8"
                          },
                          "published": false
                        }
                      },
                      "status": "failed",
                      "attempts": 2,
                      "next_attempt_at": "2026-10-05T18:08:12Z",
                      "last_status_code": 502,
                      "last_error": "HTTP 502",
                      "response_ms": 311,
                      "created_at": "2026-10-05T18:02:11Z",
                      "delivered_at": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/webhooks/{id}/deliveries/{delivery_id}/redeliver": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Queue a delivery again (same payload, signed at send time)",
        "description": "**Required scope:** `webhooks:manage`\n\nRe-queues one delivery of this endpoint — including a `dead` one — for an immediate attempt with its original\npayload (same event id); the signature and timestamp are computed fresh when it is sent. The status goes back to\n`pending`; the attempt counter keeps counting, so a delivery that fails again past the retry schedule goes\nstraight back to `dead`. Audited as `webhook.redeliver`.",
        "operationId": "redeliverOwnWebhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook endpoint id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "delivery_id",
            "in": "path",
            "required": true,
            "description": "Delivery id (from the delivery log); must belong to this endpoint",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Delivery re-queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                },
                "example": {
                  "id": "01929a77-0c3d-7e4f-8a5b-6c7d8e9f0a1b",
                  "endpoint_id": "01928f3a-6b1c-7d2e-8f40-5a6b7c8d9e0f",
                  "event_type": "asset.ready",
                  "event_id": "01929a77-0c2e-7f10-9a2b-3c4d5e6f7a8b",
                  "payload": {
                    "id": "01929a77-0c2e-7f10-9a2b-3c4d5e6f7a8b",
                    "type": "asset.ready",
                    "at": "2026-10-05T18:02:11Z",
                    "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                    "data": {
                      "asset_id": "01929a51-7d3e-7a10-b2c3-d4e5f6a7b8c9",
                      "published": false
                    }
                  },
                  "status": "pending",
                  "attempts": 7,
                  "next_attempt_at": "2026-10-06T07:31:05Z",
                  "last_status_code": 502,
                  "last_error": "HTTP 502",
                  "response_ms": 298,
                  "created_at": "2026-10-05T18:02:11Z",
                  "delivered_at": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/api-keys": {
      "get": {
        "tags": [
          "keys"
        ],
        "summary": "The tenant's API keys (never the secret); `scopes` = what the caller may grant",
        "description": "**Required scope:** `keys:manage`\n\nEvery API key of the tenant, newest first, revoked ones included (`revoked_at` set), with its 8-character\n`key_prefix`, scopes and last use. The secret part is never returned — only its argon2id hash is stored.\n`scopes` lists the caller's own scopes: the most a new key created by this caller may carry. Needs\n`keys:manage` (Engineer and up).",
        "operationId": "listOwnApiKeys",
        "responses": {
          "200": {
            "description": "Keys, newest first (revoked included)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "scopes"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKey"
                      }
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Scope"
                      },
                      "description": "The caller's scopes (grantable to a new key)"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01929d10-4a5b-7c6d-8e7f-9a0b1c2d3e4f",
                      "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                      "name": "newsroom CMS",
                      "key_prefix": "Q7mK2pXa",
                      "scopes": [
                        "assets:read",
                        "assets:write",
                        "clips:write",
                        "prewarm"
                      ],
                      "last_used_at": "2026-10-06T06:58:12Z",
                      "revoked_at": null,
                      "created_at": "2026-09-30T08:15:00Z"
                    }
                  ],
                  "scopes": [
                    "assets:read",
                    "channels:read",
                    "clips:read",
                    "stats:read",
                    "events:read",
                    "sites:read",
                    "assets:write",
                    "clips:write",
                    "uploads",
                    "sites:write",
                    "epg:write",
                    "assets:publish",
                    "clips:publish",
                    "sites:publish",
                    "epg:publish",
                    "channels:write",
                    "channels:operate",
                    "delivery:write",
                    "prewarm",
                    "webhooks:manage",
                    "storage:manage",
                    "keys:manage",
                    "sites:admin",
                    "playback:sign",
                    "security:manage"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "keys:manage"
      },
      "post": {
        "tags": [
          "keys"
        ],
        "summary": "Create an API key; the plaintext is returned once. Scopes must be a subset of the caller's scopes.",
        "description": "**Required scope:** `keys:manage`\n\nCreates a key of the form `vs_<8-char prefix>_<32-char secret>` (base62). The plaintext `key` is in this\nresponse only; ViewStream stores the prefix and an argon2id hash of the secret. Every requested scope must be a\nknown scope AND one the caller holds (a key never exceeds its creator: an Engineer cannot mint `team:manage`).\n`name` is at most 120 characters. Send the key as `Authorization: Bearer <key>`. Audited as `api_key.create`.",
        "operationId": "createOwnApiKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyCreate"
              },
              "example": {
                "name": "newsroom CMS",
                "scopes": [
                  "assets:read",
                  "assets:write",
                  "clips:write",
                  "prewarm"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created (plaintext `key` shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyCreated"
                },
                "example": {
                  "id": "01929d10-4a5b-7c6d-8e7f-9a0b1c2d3e4f",
                  "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                  "name": "newsroom CMS",
                  "key_prefix": "Q7mK2pXa",
                  "scopes": [
                    "assets:read",
                    "assets:write",
                    "clips:write",
                    "prewarm"
                  ],
                  "last_used_at": null,
                  "revoked_at": null,
                  "created_at": "2026-10-06T07:45:00Z",
                  "key": "vs_Q7mK2pXa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntegrationBadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "keys:manage"
      }
    },
    "/v1/api-keys/{id}": {
      "delete": {
        "tags": [
          "keys"
        ],
        "summary": "Revoke a key of the tenant",
        "description": "**Required scope:** `keys:manage`\n\nRevokes the key at once: requests with it answer 401 `API key is revoked`. The row stays in the list with\n`revoked_at`. Revoking an already revoked (or unknown, or another tenant's) key answers 404. A key may revoke\nitself. Audited as `api_key.revoke`.",
        "operationId": "revokeOwnApiKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "API key id (not the prefix)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "keys:manage"
      }
    },
    "/v1/inbound-hooks": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "summary": "Inbound (CMS → ViewStream) hooks of the tenant",
        "description": "**Required scope:** `webhooks:manage`\n\nLists the tenant's inbound hooks, oldest first, with their mapping and when each last received a valid call.\nSecrets are never returned (only at creation).",
        "operationId": "listInboundHooks",
        "responses": {
          "200": {
            "description": "Hooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InboundHook"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01929c02-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
                      "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                      "name": "wordpress",
                      "kind": "cms_publish",
                      "mapping": {
                        "asset_external_ids": "$.post.videos[*].id"
                      },
                      "enabled": true,
                      "created_at": "2026-09-29T11:40:00Z",
                      "last_received_at": "2026-10-05T17:58:03Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "webhooks:manage"
      },
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Create an inbound hook; its URL and signing secret are returned once",
        "description": "**Required scope:** `webhooks:manage`\n\nCreates a hook your CMS calls when it publishes an article (`POST /v1/hooks/{hook_id}`, signed with the returned\n`secret`, `hksec_` + 64 hex, shown only here). `mapping` holds JSONPath-lite expressions (`$.a.b`, `[*]`, `[n]`)\nthat pick asset external ids, asset ids or CDN URLs out of the CMS payload; at least one is required. On each\ncall the matched `ready` assets are published (only when the tenant has auto-publish on) and their manifests,\nplus the matched URLs on the tenant's CDN hostname, are pre-warmed (trigger `cms_publish`). Audited as\n`inbound_hook.create`.",
        "operationId": "createInboundHook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mapping"
                ],
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Label shown in Studio"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "cms_publish",
                      "custom"
                    ],
                    "default": "cms_publish",
                    "description": "Recorded with the hook; both kinds are processed the same way"
                  },
                  "mapping": {
                    "$ref": "#/components/schemas/InboundHookMapping"
                  }
                }
              },
              "example": {
                "name": "wordpress",
                "kind": "cms_publish",
                "mapping": {
                  "asset_external_ids": "$.post.videos[*].id",
                  "urls": "$.post.video_urls[*]"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — `secret` and `url` appear only in this response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "hook",
                    "secret",
                    "url"
                  ],
                  "properties": {
                    "hook": {
                      "$ref": "#/components/schemas/InboundHook"
                    },
                    "secret": {
                      "type": "string",
                      "description": "Signing secret (`hksec_` + 64 hex), shown once"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "The URL your CMS POSTs to"
                    }
                  }
                },
                "example": {
                  "hook": {
                    "id": "01929c02-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
                    "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
                    "name": "wordpress",
                    "kind": "cms_publish",
                    "mapping": {
                      "asset_external_ids": "$.post.videos[*].id",
                      "urls": "$.post.video_urls[*]"
                    },
                    "enabled": true,
                    "created_at": "2026-09-29T11:40:00Z",
                    "last_received_at": null
                  },
                  "secret": "hksec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
                  "url": "https://api.viewstream.co.il/v1/hooks/01929c02-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntegrationBadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/inbound-hooks/{id}": {
      "delete": {
        "tags": [
          "webhooks"
        ],
        "summary": "Delete an inbound hook",
        "description": "**Required scope:** `webhooks:manage`\n\nDeletes the hook; calls to its URL answer 404 from then on. Audited as `inbound_hook.delete`.",
        "operationId": "deleteInboundHook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Inbound hook id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-destinations": {
      "get": {
        "tags": [
          "event-export"
        ],
        "summary": "Event export destinations of the tenant",
        "description": "**Required scope:** `webhooks:manage`\n\nEvent export destinations of the tenant: statistics events (player, ads, Sites) and platform events sent as batches to the customer's endpoint or GA4\n\nLists the tenant's destinations (oldest first) with their export health (`state`: last batch outcome and\ndead-letter batches waiting), the event `catalogue` (groups player, ads, session, site, platform), the tenant's\ntest receiver URL (`inbox_url`, see `GET /v1/event-inbox`) and the PoC `limits` (5 destinations, `batch_max`\n5000, 7 days of logs and dead letters). Credentials and secrets are never returned — only `secret_hint`.\nAnswers 503 when event export is not available on this deployment.",
        "operationId": "listEventDestinations",
        "responses": {
          "200": {
            "description": "Destinations, the event catalogue and the tenant's test receiver URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "catalogue",
                    "inbox_url",
                    "limits"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventDestination"
                      }
                    },
                    "catalogue": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "enum": [
                              "player",
                              "ads",
                              "session",
                              "site",
                              "platform"
                            ]
                          },
                          "events": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "inbox_url": {
                      "type": "string",
                      "description": "The tenant's test receiver (`…/v1/event-inbox/<token>`); empty if it could not be read"
                    },
                    "limits": {
                      "type": "object",
                      "properties": {
                        "destinations": {
                          "type": "integer",
                          "description": "Maximum destinations per tenant (5)"
                        },
                        "batch_max": {
                          "type": "integer",
                          "description": "Largest allowed batch_max (5000)"
                        },
                        "retain_days": {
                          "type": "integer",
                          "description": "Days delivery logs",
                          "statistics and dead letters are kept (7)": null
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192a0c4-7d8e-7f90-a1b2-c3d4e5f60718",
                      "name": "BI",
                      "kind": "https",
                      "url": "https://bi.example.co.il/viewstream",
                      "config": {},
                      "format": "ndjson",
                      "auth": "hmac",
                      "secret_hint": "…9f3a",
                      "ga4_measurement_id": "",
                      "events": [
                        "first_frame",
                        "heartbeat",
                        "ended",
                        "asset.ready"
                      ],
                      "filters": {
                        "channels": [
                          "tv10"
                        ]
                      },
                      "sampling": 1,
                      "batch_max": 500,
                      "batch_wait_ms": 5000,
                      "ip_hash": false,
                      "enabled": true,
                      "created_at": "2026-10-01T10:00:00Z",
                      "updated_at": "2026-10-01T10:00:00Z",
                      "state": {
                        "last_at": "2026-10-06T07:29:55Z",
                        "last_status": "ok",
                        "last_error": "",
                        "dlq_pending": 0,
                        "dlq_last_at": null
                      }
                    }
                  ],
                  "catalogue": [
                    {
                      "key": "player",
                      "events": [
                        "session_start",
                        "first_frame",
                        "heartbeat",
                        "pause",
                        "resume",
                        "seek",
                        "level_switch",
                        "rebuffer",
                        "error",
                        "fullscreen",
                        "mute",
                        "visibility",
                        "ended",
                        "session_end"
                      ]
                    },
                    {
                      "key": "ads",
                      "events": [
                        "ad_request",
                        "ad_impression",
                        "ad_start",
                        "ad_q1",
                        "ad_mid",
                        "ad_q3",
                        "ad_complete",
                        "ad_skip",
                        "ad_click",
                        "ad_error",
                        "ad_pod_start",
                        "ad_pod_end"
                      ]
                    },
                    {
                      "key": "session",
                      "events": [
                        "session_summary"
                      ]
                    },
                    {
                      "key": "site",
                      "events": [
                        "page_view",
                        "section_view",
                        "tile_click",
                        "search",
                        "not_found"
                      ]
                    },
                    {
                      "key": "platform",
                      "events": [
                        "asset.ready",
                        "asset.published",
                        "asset.failed",
                        "clip.ready",
                        "clip.final",
                        "channel.feed_changed",
                        "channel.programme_started",
                        "prewarm.finished"
                      ]
                    }
                  ],
                  "inbox_url": "https://api.viewstream.co.il/v1/event-inbox/3kP9xxxxxxxxxxxxxxxxxxxx",
                  "limits": {
                    "destinations": 5,
                    "batch_max": 5000,
                    "retain_days": 7
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      },
      "post": {
        "tags": [
          "event-export"
        ],
        "summary": "Create a destination; an HMAC signing secret is returned once",
        "description": "**Required scope:** `webhooks:manage`\n\n`kind: https` POSTs batches (JSON array, or NDJSON) of `vs.event.v1` events with headers `X-VS-Destination`, `X-VS-Batch`,\n`X-VS-Event-Count`, `X-VS-Schema` and, for `auth: hmac`, `X-VS-Timestamp` + `X-VS-Signature: sha256=<hex HMAC-SHA256(secret,\ntimestamp + \".\" + body)>` (as webhooks); `auth: bearer` sends `Authorization: Bearer <token>`. Retries 1 s → 10 min, then the\nbatch goes to the dead-letter queue (7 days, replayable). `kind: ga4` sends to the GA4 Measurement Protocol\n(`ga4_measurement_id` + `api_secret`). `kind: s3` writes each batch as one gzip NDJSON object\n`<config.prefix>/dt=YYYY-MM-DD/hour=HH/<sha256[:20]>.ndjson.gz` (date/hour UTC of the batch's first event; the name\ndepends only on the content, so retries and replays overwrite the same object) to `config.bucket` at the endpoint\n`url` (path-style, SigV4, `credentials.access_key` + `credentials.secret_key`; default wait 60 s, up to 15 min).\n`kind: kafka` produces one record per event to `config.topic` on `config.brokers`, key = `session_id` (platform\nevents: channel id, else event id), headers `vs-schema`, `vs-type`; optional TLS (`config.tls`, `config.ca_pem`) and\nSASL (`config.sasl`: plain | scram-sha-256 | scram-sha-512, `config.username` + `credentials.password`).\nEvery endpoint and broker (also brokers the cluster advertises) goes through the SSRF guard: private, loopback and\nour own networks are refused except the operator allow-list `EXPORT_ALLOW_ORIGINS`. Credentials are write-only\n(sealed; omitted on update = keep). At-least-once: de-duplicate on the event `id`. Viewer IPs are never exported;\nthe viewer id is only the per-tenant hash and is absent when consent was denied; `ip_hash` (needs `stats:pii`)\nadds a per-tenant salted hash (never with denied consent).\nDefaults: `kind` https, `format` json, `auth` hmac, `sampling` 1, `batch_max` 500 (s3 5000), `batch_wait_ms`\n5000 (s3 60000). `name` 1–120 characters; filters: up to 50 channel slugs, 200 asset ids, 250 country codes.\nThe HMAC secret (`evsec_` + 64 hex) is returned only here and on rotation. Audited as `event_destination.create`.",
        "operationId": "createEventDestination",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventDestinationInput"
              },
              "example": {
                "name": "BI",
                "url": "https://bi.example.co.il/viewstream",
                "format": "ndjson",
                "auth": "hmac",
                "events": [
                  "first_frame",
                  "heartbeat",
                  "ended",
                  "asset.ready"
                ],
                "sampling": 1
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created (secret shown once for auth hmac)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventDestination"
                },
                "example": {
                  "id": "0192a0c4-7d8e-7f90-a1b2-c3d4e5f60718",
                  "name": "BI",
                  "kind": "https",
                  "url": "https://bi.example.co.il/viewstream",
                  "config": {},
                  "format": "ndjson",
                  "auth": "hmac",
                  "secret_hint": "…cdef",
                  "secret": "evsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
                  "ga4_measurement_id": "",
                  "events": [
                    "first_frame",
                    "heartbeat",
                    "ended",
                    "asset.ready"
                  ],
                  "filters": {},
                  "sampling": 1,
                  "batch_max": 500,
                  "batch_wait_ms": 5000,
                  "ip_hash": false,
                  "enabled": true,
                  "created_at": "2026-10-06T07:30:00Z",
                  "updated_at": "2026-10-06T07:30:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntegrationBadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/EventDestinationLimit"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-destinations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "patch": {
        "tags": [
          "event-export"
        ],
        "summary": "Edit, pause/resume (`enabled`) or rotate the HMAC secret (`rotate_secret: true` → returned once)",
        "description": "**Required scope:** `webhooks:manage`\n\nPartial update with the same fields and rules as create; the merged destination is validated again. A body\nwith only `enabled: false` (pause) skips the reachability check, so a destination whose target became disallowed\ncan still be paused. Secrets: a new HMAC secret is generated (and returned once, in `secret`) on\n`rotate_secret: true` or when switching to `auth: hmac`; switching to `auth: bearer` needs `token`; `auth: none`\ndrops the stored secret; s3/kafka `credentials` fields left empty keep the stored ones (changing `kind` needs\nfresh ones). Setting `ip_hash: true` needs `stats:pii`. Audited as `event_destination.update`.",
        "operationId": "patchEventDestination",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventDestinationInput"
              },
              "example": {
                "rotate_secret": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved (`secret` only after a rotation / a switch to hmac)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventDestination"
                },
                "example": {
                  "id": "0192a0c4-7d8e-7f90-a1b2-c3d4e5f60718",
                  "name": "BI",
                  "kind": "https",
                  "url": "https://bi.example.co.il/viewstream",
                  "config": {},
                  "format": "ndjson",
                  "auth": "hmac",
                  "secret_hint": "…4321",
                  "secret": "evsec_fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
                  "ga4_measurement_id": "",
                  "events": [
                    "first_frame",
                    "heartbeat",
                    "ended",
                    "asset.ready"
                  ],
                  "filters": {},
                  "sampling": 1,
                  "batch_max": 500,
                  "batch_wait_ms": 5000,
                  "ip_hash": false,
                  "enabled": true,
                  "created_at": "2026-10-01T10:00:00Z",
                  "updated_at": "2026-10-06T07:35:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntegrationBadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      },
      "delete": {
        "tags": [
          "event-export"
        ],
        "summary": "Delete a destination with its delivery log and dead-letter queue",
        "description": "**Required scope:** `webhooks:manage`\n\nDeletes the destination; its delivery log, statistics and dead-letter batches go with it and the exporter stops sending to it. Audited as `event_destination.delete`.",
        "operationId": "deleteEventDestination",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-destinations/{id}/test": {
      "post": {
        "tags": [
          "event-export"
        ],
        "summary": "Send one synthetic `test` event now and return the outcome",
        "description": "**Required scope:** `webhooks:manage`\n\nSends one synthetic event (`type: test`, `data.message: \"ViewStream event export test\"`) synchronously through\nthe destination's own transport, auth and format — a one-event batch, no retries — and answers with the\noutcome; it is also written to the delivery log as kind `test`. Works on a paused destination too. The answer\nis 200 even when the send failed: read `ok`, `http_status` and `error`.",
        "operationId": "testEventDestination",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Event destination id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Outcome",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "latency_ms",
                    "error",
                    "event"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "2xx (https/ga4), object written (s3) or records acknowledged (kafka)"
                    },
                    "http_status": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "The receiver's HTTP status (null when none was received)"
                    },
                    "latency_ms": {
                      "type": "integer"
                    },
                    "error": {
                      "type": "string",
                      "description": "Empty on success; else the transport error or `HTTP <status>`"
                    },
                    "ref": {
                      "type": "string",
                      "description": "s3: bucket/object key written; kafka: topic, partitions, first offset"
                    },
                    "event": {
                      "$ref": "#/components/schemas/ExportEvent"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "http_status": 200,
                  "latency_ms": 84,
                  "error": "",
                  "ref": "",
                  "event": {
                    "schema": "vs.event.v1",
                    "id": "0192a0d1-0b1c-7d2e-8f30-415263748596",
                    "type": "test",
                    "at": "2026-10-06T07:31:00Z",
                    "tenant": "tv10poc",
                    "data": {
                      "message": "ViewStream event export test"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-destinations/{id}/deliveries": {
      "get": {
        "tags": [
          "event-export"
        ],
        "summary": "Batch delivery log of a destination (newest first, 7 days)",
        "description": "**Required scope:** `webhooks:manage`\n\nOne row per batch attempt outcome (kind `batch`, `test` or `replay`) with size, status, HTTP status, latency,\nerror and a sample line, newest first. No cursor: `limit` (default 50, at most 200; other values fall back to\n50) bounds the page. Rows are kept 7 days.",
        "operationId": "listEventDeliveries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Event destination id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Rows to return; outside 1–200 (or absent) = 50",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Log",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventDelivery"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": 48213,
                      "at": "2026-10-06T07:29:55Z",
                      "kind": "batch",
                      "events": 412,
                      "bytes": 301877,
                      "status": "ok",
                      "http_status": 200,
                      "latency_ms": 96,
                      "attempt": 1,
                      "error": "",
                      "sample": "{\"schema\":\"vs.event.v1\",\"id\":\"0192a0cf-…\",\"type\":\"heartbeat\",…}",
                      "ref": ""
                    },
                    {
                      "id": 48190,
                      "at": "2026-10-06T07:12:03Z",
                      "kind": "batch",
                      "events": 500,
                      "bytes": 366210,
                      "status": "retrying",
                      "http_status": 503,
                      "latency_ms": 1203,
                      "attempt": 1,
                      "error": "HTTP 503",
                      "sample": "",
                      "ref": ""
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-destinations/{id}/dlq": {
      "get": {
        "tags": [
          "event-export"
        ],
        "summary": "Dead-letter batches of a destination (batches that failed every retry; kept 7 days)",
        "description": "**Required scope:** `webhooks:manage`\n\nThe newest 100 dead-letter batches of the destination (without their bodies), with the last error and the\nreplay state: `replay_requested_at` set = waiting for the exporter, `replayed_at` set = sent again.",
        "operationId": "listEventDLQ",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Event destination id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Batches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventDLQBatch"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192a0b9-2c3d-7e4f-8051-62738495a6b7",
                      "created_at": "2026-10-05T23:41:12Z",
                      "events": 500,
                      "last_error": "HTTP 503",
                      "replay_requested_at": null,
                      "replayed_at": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-destinations/{id}/dlq/replay": {
      "post": {
        "tags": [
          "event-export"
        ],
        "summary": "Replay one dead-letter batch (`batch`) or all of them (empty body); the exporter picks them up within 30 s",
        "description": "**Required scope:** `webhooks:manage`\n\nMarks one dead-letter batch (`batch`), or every batch not replayed yet (no body, `{}` or an unreadable body), for\nreplay; the exporter sends them again with their original content within about 30 s (one attempt each; a failed\nreplay — or one for a paused destination — stays in the queue with the request cleared and `last_error` updated). Answers 202 with the number of batches queued, 404 when nothing was waiting.\nAudited as `event_destination.replay`.",
        "operationId": "replayEventDLQ",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Event destination id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "batch": {
                    "type": "string",
                    "format": "uuid",
                    "description": "One dead-letter batch id; omit to replay every pending batch"
                  }
                }
              },
              "example": {
                "batch": "0192a0b9-2c3d-7e4f-8051-62738495a6b7"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "queued"
                  ],
                  "properties": {
                    "queued": {
                      "type": "integer",
                      "description": "Batches marked for replay"
                    }
                  }
                },
                "example": {
                  "queued": 1
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such destination, or no un-replayed batch matched (`not_found`, \"nothing to replay\")",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`batch` is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-destinations/{id}/stats": {
      "get": {
        "tags": [
          "event-export"
        ],
        "summary": "Export statistics of a destination",
        "description": "**Required scope:** `webhooks:manage`\n\nExport statistics of a destination: per-minute events sent, failed attempts, dead-letter batches, dropped events, backlog and lag (default the last 24 h), with a summary\n\nWritten by the exporter for every finished minute with traffic (minutes without traffic are absent — treat them\nas zero). `lag_ms_*` is the time from an event entering the destination's queue to its batch being delivered\n(the batch's oldest event). `dropped` counts events refused because the destination's 20 000-event queue was\nfull. Kept 7 days. The window ends at the start of the next minute; `summary.backlog` is the newest sample when\nit is at most 2 minutes old, else 0.",
        "operationId": "getEventDestinationStats",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Event destination id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "hours",
            "in": "query",
            "description": "Window length in hours ending now (1–168)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 168,
              "default": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Statistics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventDestinationStats"
                },
                "example": {
                  "from": "2026-10-06T06:32:00Z",
                  "to": "2026-10-06T07:32:00Z",
                  "step_s": 60,
                  "points": [
                    {
                      "t": "2026-10-06T07:30:00Z",
                      "sent": 1840,
                      "batches_ok": 5,
                      "failed": 0,
                      "dead": 0,
                      "dead_events": 0,
                      "dropped": 0,
                      "backlog": 120,
                      "lag_ms_max": 5400,
                      "lag_ms_avg": 2600
                    }
                  ],
                  "summary": {
                    "sent_per_min": 1792.4,
                    "sent": 103211,
                    "failed": 2,
                    "dead": 0,
                    "dead_events": 0,
                    "dropped": 0,
                    "lag_ms_avg": 2600,
                    "lag_ms_max": 61200,
                    "backlog": 120,
                    "last_error": "HTTP 503",
                    "last_error_at": "2026-10-06T07:12:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "`hours` is not an integer in 1–168",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-inbox": {
      "get": {
        "tags": [
          "event-export"
        ],
        "summary": "The tenant's test receiver",
        "description": "**Required scope:** `webhooks:manage`\n\nThe tenant's test receiver: its URL (use it as a destination to try export) and the last 50 requests it received, with the HMAC signature checked against the tenant's destinations (memory only; cleared on restart)\n\nUse `url` as the URL of an `https` destination to see exactly what ViewStream sends. Returns the last 50\nrequests the receiver accepted, newest first, with the `X-VS-*` headers, a masked bearer token, the body (first\n64 KB) and `verified`: `valid` when `X-VS-Signature` matches one of the tenant's HMAC destination secrets\n(timestamp within 24 h), `invalid` when signed but no secret matches, `unsigned` otherwise. Kept in memory of\none API instance only.",
        "operationId": "getEventInbox",
        "responses": {
          "200": {
            "description": "Inbox",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "items"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "The tenant's test receiver URL"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventInboxItem"
                      }
                    }
                  }
                },
                "example": {
                  "url": "https://api.viewstream.co.il/v1/event-inbox/3kP9xxxxxxxxxxxxxxxxxxxx",
                  "items": [
                    {
                      "at": "2026-10-06T07:31:00Z",
                      "content_type": "application/json",
                      "destination": "0192a0c4-7d8e-7f90-a1b2-c3d4e5f60718",
                      "batch": "0192a0d1-0b1d-7a2b-8c3d-4e5f60718293",
                      "event_count": "1",
                      "timestamp": "1791271860",
                      "signature": "sha256=5f1c…",
                      "authorization": "",
                      "bytes": 187,
                      "body": "[{\"schema\":\"vs.event.v1\",\"id\":\"0192a0d1-0b1c-7d2e-8f30-415263748596\",\"type\":\"test\",\"at\":\"2026-10-06T07:31:00Z\",\"tenant\":\"tv10poc\",\"data\":{\"message\":\"ViewStream event export test\"}}]",
                      "verified": "valid"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      },
      "delete": {
        "tags": [
          "event-export"
        ],
        "summary": "Clear the test receiver",
        "description": "**Required scope:** `webhooks:manage`\n\nForgets every request the tenant's test receiver kept (on this API instance). The receiver URL stays the same.",
        "operationId": "clearEventInbox",
        "responses": {
          "204": {
            "description": "Cleared"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/EventExportUnavailable"
          }
        },
        "x-required-scope": "webhooks:manage"
      }
    },
    "/v1/event-inbox/{token}": {
      "post": {
        "tags": [
          "event-export"
        ],
        "summary": "Public test receiver (no API key; the token in the path identifies the tenant)",
        "description": "Public test receiver (no API key; the token in the path identifies the tenant). Accepts any body up to 2 MB and answers 204.\n\nThe endpoint behind `inbox_url`. No authentication: the token in the path identifies the tenant. Keeps the\nrequest (headers `Content-Type`, `X-VS-Destination`, `X-VS-Batch`, `X-VS-Event-Count`, `X-VS-Timestamp`,\n`X-VS-Signature`, the last 4 characters of a bearer token, and the body) for `GET /v1/event-inbox`; nothing is\nforwarded. Errors have no body.",
        "operationId": "receiveEventInbox",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The tenant's inbox token (from `inbox_url`)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ExportEvent"
                }
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "One ExportEvent JSON object per line"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Received"
          },
          "404": {
            "description": "Unknown token",
            "or event export not available (empty body)": null
          },
          "413": {
            "description": "Body larger than 2 MB (empty body)"
          }
        },
        "security": []
      }
    },
    "/v1/hooks/{hook_id}": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Signed CMS call (no API key); answers 202 immediately and processes asynchronously",
        "description": "Called by your CMS, without an API key: the request is authenticated by `X-VS-Timestamp` (unix seconds, within\n±5 min of the server clock) and `X-VS-Signature: sha256=<hex HMAC-SHA256(hook secret, timestamp + \".\" + raw body)>`,\nexactly as outbound webhooks are signed. The body (JSON, at most 1 MB) is run through the hook's mapping and the\nanswer (202) lists what was `found`. Processing then happens in the background: matched `ready` assets of the\ntenant are published if the tenant auto-publishes, and their manifests plus matched URLs on the tenant's CDN\nhostname are pre-warmed (one run per distinct set, trigger `cms_publish`). Unknown or disabled hooks answer 404.",
        "operationId": "receiveHook",
        "parameters": [
          {
            "name": "hook_id",
            "in": "path",
            "required": true,
            "description": "Inbound hook id (the last path segment of the hook URL)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "X-VS-Timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds; must be within ±5 minutes",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-VS-Signature",
            "in": "header",
            "required": true,
            "description": "`sha256=` + hex HMAC-SHA256 of `<timestamp>.<raw body>` with the hook secret",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "description": "Any JSON document (≤ 1 MB); the hook's mapping decides what is read"
              },
              "example": {
                "post": {
                  "id": 123,
                  "videos": [
                    {
                      "id": "tv10-2026-0928-01"
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted; processing continues asynchronously",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "accepted",
                    "found"
                  ],
                  "properties": {
                    "accepted": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "request_id": {
                      "type": "string",
                      "description": "The request id (also in X-Request-Id)"
                    },
                    "found": {
                      "$ref": "#/components/schemas/InboundHookExtracted"
                    }
                  }
                },
                "example": {
                  "accepted": true,
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
                  "found": {
                    "asset_external_ids": [
                      "tv10-2026-0928-01"
                    ],
                    "asset_ids": [],
                    "urls": []
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid signature or a timestamp outside ±5 min (`invalid_credentials`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/invalid_credentials",
                  "title": "Missing or invalid API key",
                  "status": 401,
                  "detail": "invalid signature: X-VS-Timestamp outside ±5m0s",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "description": "Body larger than 1 MB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The body is not JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/IntegrationInternalError"
          }
        },
        "security": []
      }
    },
    "/v1/series": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Shows (series) of the tenant, with VOD and catch-up episode counts (sites:read)",
        "description": "**Required scope:** `sites:read`\n\nLists every show, podcast and collection-show of the tenant, ordered by `sort` then `title` (no pagination).\n`vod_count` counts the VOD episodes whose asset is not deleted; `catchup_count` counts the EPG programmes the\nmatcher attached to the show. Needs `sites:read`; platform tenants only (CDN-only tenants get 403).",
        "operationId": "listSeries",
        "responses": {
          "200": {
            "description": "The shows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Series"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192a3f1-6c2e-7d41-9b0a-1c2d3e4f5a61",
                      "title": "סוגרים שוק",
                      "description": "מגזין הכלכלה היומי",
                      "i18n": {
                        "en": {
                          "title": "Market Close"
                        }
                      },
                      "kind": "show",
                      "slug": "סוגרים-שוק",
                      "epg_match": {
                        "channel": "main",
                        "titles": [
                          "^סוגרים שוק$"
                        ]
                      },
                      "images": {},
                      "seo": {},
                      "collection_id": null,
                      "sort": 0,
                      "created_at": "2026-09-28T09:12:44Z",
                      "updated_at": "2026-10-02T14:03:10Z",
                      "vod_count": 12,
                      "catchup_count": 41
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Create a show: title, description, i18n, kind show|podcast|collection, slug (from the title when omitted)…",
        "description": "**Required scope:** `sites:write`\n\nCreate a show: title, description, i18n, kind show|podcast|collection, slug (from the title when omitted), epg_match {channel, titles[] (case-insensitive Postgres regexes), external_ids[]}, images, seo, collection_id, sort (sites:write)\n\nCreates a show. `title` is required (≤ 300 chars); `slug` defaults to the title slugified (Hebrew letters are\nkept, other runs become `-`) and must be ≤ 120 chars without `/ ? # %` or spaces, unique in the tenant (409).\nWith an `epg_match` the matcher runs at once: programmes of `channel` (slug; omitted = any channel) whose title\nmatches one of `titles` (≤ 20 case-insensitive Postgres regexes, 1–200 chars each) or whose EPG external id is in\n`external_ids` (≤ 500) become the show's catch-up episodes (the orchestrator re-runs it every 2 minutes).\nBody ≤ 64 KB, unknown fields are refused (400). Purges the show's Sites cache tag `entity:series:<id>`.",
        "operationId": "createSeries",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeriesInput"
              },
              "example": {
                "title": "הדבר הבא",
                "description": "תוכנית הטכנולוגיה של ערוץ 10",
                "kind": "show",
                "i18n": {
                  "en": {
                    "title": "The Next Thing"
                  }
                },
                "epg_match": {
                  "channel": "main",
                  "titles": [
                    "^הדבר הבא$"
                  ]
                },
                "sort": 10
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The show (counts are 0 here; the list fills them)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Series"
                },
                "example": {
                  "id": "0192a3f1-6c2e-7d41-9b0a-1c2d3e4f5a62",
                  "title": "הדבר הבא",
                  "description": "תוכנית הטכנולוגיה של ערוץ 10",
                  "i18n": {
                    "en": {
                      "title": "The Next Thing"
                    }
                  },
                  "kind": "show",
                  "slug": "הדבר-הבא",
                  "epg_match": {
                    "channel": "main",
                    "titles": [
                      "^הדבר הבא$"
                    ]
                  },
                  "images": {},
                  "seo": {},
                  "collection_id": null,
                  "sort": 10,
                  "created_at": "2026-10-06T08:15:00Z",
                  "updated_at": "2026-10-06T08:15:00Z",
                  "vod_count": 0,
                  "catchup_count": 0
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/series/{id}": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "One show",
        "description": "**Required scope:** `sites:read`\n\nThe show with its settings. `vod_count` and `catchup_count` are always 0 here (only the list fills them). Needs `sites:read`.",
        "operationId": "getSeries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Show id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The show",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Series"
                },
                "example": {
                  "id": "0192a3f1-6c2e-7d41-9b0a-1c2d3e4f5a61",
                  "title": "סוגרים שוק",
                  "description": "מגזין הכלכלה היומי",
                  "i18n": {},
                  "kind": "show",
                  "slug": "סוגרים-שוק",
                  "epg_match": {
                    "channel": "main",
                    "titles": [
                      "^סוגרים שוק$"
                    ]
                  },
                  "images": {},
                  "seo": {},
                  "collection_id": null,
                  "sort": 0,
                  "created_at": "2026-09-28T09:12:44Z",
                  "updated_at": "2026-10-02T14:03:10Z",
                  "vod_count": 0,
                  "catchup_count": 0
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "patch": {
        "tags": [
          "sites"
        ],
        "summary": "Update a show; a changed epg_match re-runs the matcher at once",
        "description": "**Required scope:** `sites:write`\n\nChanges only the fields sent (same rules as create; `title` may not become empty, a `slug` sent empty is refused).\n`epg_match: null` stops matching and releases the show's programmes on the next run; sending `epg_match` re-runs\nthe matcher at once. `collection_id` set to the nil UUID (`00000000-0000-0000-0000-000000000000`) clears the\nsection. Body ≤ 64 KB. Purges `entity:series:<id>`. Needs `sites:write`.",
        "operationId": "patchSeries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Show id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeriesInput"
              },
              "example": {
                "description": "מגזין הכלכלה היומי עם סיכום המסחר",
                "epg_match": {
                  "channel": "main",
                  "titles": [
                    "^סוגרים שוק",
                    "^סוגרים שבוע$"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The show after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Series"
                },
                "example": {
                  "id": "0192a3f1-6c2e-7d41-9b0a-1c2d3e4f5a61",
                  "title": "סוגרים שוק",
                  "description": "מגזין הכלכלה היומי עם סיכום המסחר",
                  "i18n": {},
                  "kind": "show",
                  "slug": "סוגרים-שוק",
                  "epg_match": {
                    "channel": "main",
                    "titles": [
                      "^סוגרים שוק",
                      "^סוגרים שבוע$"
                    ]
                  },
                  "images": {},
                  "seo": {},
                  "collection_id": null,
                  "sort": 0,
                  "created_at": "2026-09-28T09:12:44Z",
                  "updated_at": "2026-10-06T08:15:00Z",
                  "vod_count": 0,
                  "catchup_count": 0
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      },
      "delete": {
        "tags": [
          "sites"
        ],
        "summary": "Delete a show (its assets and programmes stay)",
        "description": "**Required scope:** `sites:write`\n\nDeletes the show. Its assets and programmes are kept; their links to the show (episodes, presenters, matched programmes) are cleared. Purges `entity:series:<id>`. Needs `sites:write`.",
        "operationId": "deleteSeries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Show id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/series/{id}/episodes": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "VOD episodes (ordered) and matched catch-up programmes",
        "description": "**Required scope:** `sites:read`\n\n`vod` lists the show's VOD episodes (assets not deleted) ordered by season, episode, then position. `catchup`\nlists up to 500 programmes the EPG matcher attached to the show, newest first. An id that is not a show of the\ntenant answers 200 with two empty lists (only a malformed id answers 404). Needs `sites:read`.",
        "operationId": "listSeriesEpisodes",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Show id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "VOD episodes and catch-up programmes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeriesEpisodes"
                },
                "example": {
                  "vod": [
                    {
                      "asset_id": "0192b0c4-11aa-7e3b-8c5d-2f1e0a9b8c71",
                      "title": "סוגרים שוק — פרק 1",
                      "season": 1,
                      "episode": 1,
                      "position": 0,
                      "duration_s": 1520,
                      "status": "ready"
                    }
                  ],
                  "catchup": [
                    {
                      "id": "0192c7d2-5b10-7a8e-9f21-3c4d5e6f7a81",
                      "channel": "main",
                      "start_at": "2026-10-05T16:00:00Z",
                      "end_at": "2026-10-05T16:30:00Z",
                      "title": "סוגרים שוק"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "put": {
        "tags": [
          "sites"
        ],
        "summary": "Replace the VOD episodes",
        "description": "**Required scope:** `sites:write`\n\nReplace the VOD episodes: items [{asset_id, season?, episode?}], array order = position; an asset moves from its previous show\n\nReplaces the show's VOD episode list in one transaction: array order becomes `position`; an asset can belong to one\nshow only, so listing it here removes it from its previous show. At most 2000 items, body ≤ 1 MB; every asset must\nbe a non-deleted asset of the tenant (else 422). Answers like GET `/episodes`. Purges `entity:series:<id>`.\nNeeds `sites:write`.",
        "operationId": "putSeriesEpisodes",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Show id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "maxItems": 2000,
                    "items": {
                      "type": "object",
                      "required": [
                        "asset_id"
                      ],
                      "properties": {
                        "asset_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "season": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "episode": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "asset_id": "0192b0c4-11aa-7e3b-8c5d-2f1e0a9b8c71",
                    "season": 1,
                    "episode": 1
                  },
                  {
                    "asset_id": "0192b0c4-11aa-7e3b-8c5d-2f1e0a9b8c72",
                    "season": 1,
                    "episode": 2
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The episodes after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeriesEpisodes"
                },
                "example": {
                  "vod": [
                    {
                      "asset_id": "0192b0c4-11aa-7e3b-8c5d-2f1e0a9b8c71",
                      "title": "פרק 1",
                      "season": 1,
                      "episode": 1,
                      "position": 0,
                      "duration_s": 1520,
                      "status": "ready"
                    },
                    {
                      "asset_id": "0192b0c4-11aa-7e3b-8c5d-2f1e0a9b8c72",
                      "title": "פרק 2",
                      "season": 1,
                      "episode": 2,
                      "position": 1,
                      "duration_s": 1488,
                      "status": "ready"
                    }
                  ],
                  "catchup": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/series/{id}/match": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Run the EPG matcher for this show now: {matched}",
        "description": "**Required scope:** `sites:write`\n\nRe-runs the EPG matcher for this show at once (it also runs every 2 minutes): programmes from the last 35 days and\nupcoming ones that match `epg_match` are attached, those that no longer match are released. `matched` is the\nnumber of programmes the show has in that window afterwards. Without an `epg_match` all programmes are released\nand `matched` is 0. 422 when the stored `epg_match` is invalid. Purges `entity:series:<id>`. Needs `sites:write`.",
        "operationId": "matchSeries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Show id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Programmes attached to the show in the matching window",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matched": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "matched": 41
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/series/{id}/people": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "The show's presenters, in rank order",
        "description": "**Required scope:** `sites:read`\n\nThe people linked to a show of the tenant, ordered by `rank` then Hebrew name. `link_role` is the role in this show (free text, e.g. מגיש) and `rank` the position; the Studio series drawer reads this back and `PUT` replaces the list. Needs `sites:read`.",
        "operationId": "getSeriesPeople",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Show id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The show's presenters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Person"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "link_role": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "rank": {
                                "type": "integer"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c01",
                      "slug": "dana-levi",
                      "name": {
                        "he": "דנה לוי"
                      },
                      "role": {
                        "he": "מגישה"
                      },
                      "bio": {},
                      "image_key": null,
                      "links": {},
                      "link_role": "מגישה",
                      "rank": 0
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "put": {
        "tags": [
          "sites"
        ],
        "summary": "Replace the presenters: items [{person_id, role?, rank?}]",
        "description": "**Required scope:** `sites:write`\n\nReplaces the show's presenters. `rank` orders them (default: the array index); `role` is free text shown on the\nsite. Every person must belong to the tenant (else 422). Body ≤ 256 KB. Purges `entity:series:<id>`.\nNeeds `sites:write`.",
        "operationId": "putSeriesPeople",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Show id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "person_id"
                      ],
                      "properties": {
                        "person_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "role": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "e.g. host, co-host"
                        },
                        "rank": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "Order on the site; default = position in the array"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "person_id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c01",
                    "role": "מגיש"
                  },
                  {
                    "person_id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c02",
                    "role": "פרשן"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Saved"
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/people": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "People (presenters)",
        "description": "**Required scope:** `sites:read`\n\nEvery person of the tenant, ordered by Hebrew name then slug (no pagination). Needs `sites:read`.",
        "operationId": "listPeople",
        "responses": {
          "200": {
            "description": "The people",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Person"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c01",
                      "slug": "dana-levi",
                      "name": {
                        "he": "דנה לוי",
                        "en": "Dana Levi"
                      },
                      "role": {
                        "he": "מגישה"
                      },
                      "bio": {},
                      "image_key": null,
                      "links": {}
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Create a person: slug, name L, role L, bio L, image_key, links",
        "description": "**Required scope:** `sites:write`\n\nCreates a person (presenter, guest). `name` is required and must not be empty; `name`, `role` and `bio` are\nlocalised (a string or a `{he, en, ar, ru}` map). `slug` is optional and unique in the tenant (409); it is used\nin `/person/:slug` URLs on the sites. Body ≤ 64 KB. Needs `sites:write`.",
        "operationId": "createPerson",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PersonInput"
              },
              "example": {
                "slug": "dana-levi",
                "name": {
                  "he": "דנה לוי",
                  "en": "Dana Levi"
                },
                "role": {
                  "he": "מגישה",
                  "en": "Host"
                },
                "bio": {
                  "he": "כתבת כלכלה ותיקה"
                },
                "links": {
                  "x": "https://x.com/example"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The person",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                },
                "example": {
                  "id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c01",
                  "slug": "dana-levi",
                  "name": {
                    "he": "דנה לוי",
                    "en": "Dana Levi"
                  },
                  "role": {
                    "he": "מגישה",
                    "en": "Host"
                  },
                  "bio": {
                    "he": "כתבת כלכלה ותיקה"
                  },
                  "image_key": null,
                  "links": {
                    "x": "https://x.com/example"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/people/{id}": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "A person with the shows and assets they are linked to: {person, series[], assets[]}",
        "description": "**Required scope:** `sites:read`\n\nThe person plus the shows they present (by rank) and up to 200 non-deleted assets they appear in (newest first). Needs `sites:read`.",
        "operationId": "getPerson",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Person id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The person and their links",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "person": {
                      "$ref": "#/components/schemas/Person"
                    },
                    "series": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "role": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "assets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "role": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "person": {
                    "id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c01",
                    "slug": "dana-levi",
                    "name": {
                      "he": "דנה לוי"
                    },
                    "role": {
                      "he": "מגישה"
                    },
                    "bio": {},
                    "image_key": null,
                    "links": {}
                  },
                  "series": [
                    {
                      "id": "0192a3f1-6c2e-7d41-9b0a-1c2d3e4f5a61",
                      "title": "סוגרים שוק",
                      "slug": "סוגרים-שוק",
                      "role": "מגישה"
                    }
                  ],
                  "assets": [
                    {
                      "id": "0192b0c4-11aa-7e3b-8c5d-2f1e0a9b8c71",
                      "title": "סוגרים שוק — פרק 1",
                      "role": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "patch": {
        "tags": [
          "sites"
        ],
        "summary": "Update a person",
        "description": "**Required scope:** `sites:write`\n\nChanges only the fields sent (same fields as create; `slug` unique in the tenant, 409). Body ≤ 64 KB. Purges the Sites cache tag `entity:person:<id>`. Needs `sites:write`.",
        "operationId": "patchPerson",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Person id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PersonInput"
              },
              "example": {
                "bio": {
                  "he": "כתבת כלכלה ומגישת סוגרים שוק",
                  "en": "Economics reporter, host of Market Close"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The person after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                },
                "example": {
                  "id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c01",
                  "slug": "dana-levi",
                  "name": {
                    "he": "דנה לוי",
                    "en": "Dana Levi"
                  },
                  "role": {
                    "he": "מגישה"
                  },
                  "bio": {
                    "he": "כתבת כלכלה ומגישת סוגרים שוק",
                    "en": "Economics reporter, host of Market Close"
                  },
                  "image_key": null,
                  "links": {}
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      },
      "delete": {
        "tags": [
          "sites"
        ],
        "summary": "Delete a person",
        "description": "**Required scope:** `sites:write`\n\nDeletes the person and their links to shows and assets. Needs `sites:write`.",
        "operationId": "deletePerson",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Person id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/sites": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Sites of the tenant",
        "description": "**Required scope:** `sites:read`\n\nEvery site of the tenant with its settings, plus `domain` — the zone default hostnames live in\n(`<slug>.viewstream.co.il`). `url` is `https://` + the first hostname (the canonical host: an active custom\ndomain once there is one). Needs `sites:read`; platform tenants only.",
        "operationId": "listSites",
        "responses": {
          "200": {
            "description": "The sites",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Site"
                      }
                    },
                    "domain": {
                      "type": "string",
                      "description": "Zone of the default hostnames",
                      "example": "viewstream.co.il"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a51",
                      "slug": "tv10poc",
                      "name": "ערוץ 10 כלכלה",
                      "hostnames": [
                        "tv10poc.viewstream.co.il"
                      ],
                      "locales": [
                        "he",
                        "en"
                      ],
                      "default_locale": "he",
                      "theme_id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a52",
                      "status": "live",
                      "settings": {
                        "epg": {
                          "days_back": 7,
                          "days_forward": 7
                        },
                        "headless": {
                          "enabled": false
                        }
                      },
                      "created_at": "2026-09-28T10:00:00Z",
                      "updated_at": "2026-10-05T12:40:00Z",
                      "url": "https://tv10poc.viewstream.co.il"
                    }
                  ],
                  "domain": "viewstream.co.il"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Create a site (sites:admin)",
        "description": "**Required scope:** `sites:admin`\n\nCreate a site (sites:admin). hostnames default to [<slug>.viewstream.co.il] (reserved labels refused); a default theme is created\n\nCreates a site. `slug` defaults to the tenant slug (a-z, 0-9, `-`, ≤ 63; unique across ViewStream, 409). Without\n`hostnames` the site gets `<slug>.viewstream.co.il`; reserved labels (api, studio, www, cdn, …) and hostnames used\nby another site are refused (422). `locales` default to `[he]` and `default_locale` to `he`; when `locales` is sent,\n`default_locale` must be one of them. `status` defaults to `draft`. The first theme takes the tenant branding\ncolours (or the default theme when those fail WCAG AA). Custom domains are added with `POST /v1/sites/{id}/domains`.\nBody ≤ 256 KB. Needs `sites:admin`.",
        "operationId": "createSite",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SiteInput"
              },
              "example": {
                "slug": "now14poc",
                "name": "עכשיו 14",
                "locales": [
                  "he",
                  "en"
                ],
                "default_locale": "he"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                },
                "example": {
                  "id": "0192d1e2-3f40-7a51-8b62-9c7d8e9f0a11",
                  "slug": "now14poc",
                  "name": "עכשיו 14",
                  "hostnames": [
                    "now14poc.viewstream.co.il"
                  ],
                  "locales": [
                    "he",
                    "en"
                  ],
                  "default_locale": "he",
                  "theme_id": "0192d1e2-3f40-7a51-8b62-9c7d8e9f0a12",
                  "status": "draft",
                  "settings": {},
                  "created_at": "2026-10-06T08:15:00Z",
                  "updated_at": "2026-10-06T08:15:00Z",
                  "url": "https://now14poc.viewstream.co.il"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:admin"
      }
    },
    "/v1/sites/{id}/domains": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Custom domains of a site: Cloudflare for SaaS status, DNS records to add; active ones are served",
        "description": "**Required scope:** `sites:admin`\n\nThe site's custom domains and the CNAME target customers point them at. When Cloudflare for SaaS is configured,\neach provisioned domain is refreshed from Cloudflare (at most every 20 s), domains saved before it was configured\nare provisioned now, and domains Cloudflare reports `active` join the site's `hostnames` (first, so they become\nthe canonical host); a change purges the site cache. `note` explains what is missing when SaaS is not configured.\nNeeds `sites:admin`.",
        "operationId": "listSiteDomains",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The custom domains",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteDomains"
                },
                "example": {
                  "items": [
                    {
                      "hostname": "vod.example.co.il",
                      "status": "pending",
                      "cf_id": "5f1c2d3e4a5b6c7d8e9f0a1b2c3d4e5f",
                      "cname_target": "sites-origin.viewstream.co.il",
                      "txt": [
                        {
                          "type": "TXT",
                          "name": "_cf-custom-hostname.vod.example.co.il",
                          "value": "4a1b2c3d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                        }
                      ],
                      "created_at": "2026-10-06T08:15:00Z",
                      "checked_at": "2026-10-06T08:20:00Z"
                    }
                  ],
                  "cname_target": "sites-origin.viewstream.co.il",
                  "saas_configured": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:admin"
      },
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Add a custom domain (vod.example.co.il) to a site",
        "description": "**Required scope:** `sites:admin`\n\nAdd a custom domain (vod.example.co.il) to a site; provisions a Cloudflare custom hostname when Cloudflare for SaaS is configured\n\nAdds a custom domain (lower-cased, trailing dot removed). It must be a valid subdomain (an apex cannot CNAME)\nand not one of ViewStream's own domains; at most 10 per site (422). A hostname already on this or another site\nis 409. With Cloudflare for SaaS configured a custom hostname is created at once (`pending` until the customer\nadds the CNAME and TXT records; `error` with `message` when Cloudflare refuses); otherwise the domain is saved as\n`needs_cloudflare_saas` and provisioned on a later GET. It is served only once `active`. Audited\n(`site.domain_added`). Body ≤ 4 KB. Needs `sites:admin`.",
        "operationId": "addSiteDomain",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "hostname"
                ],
                "properties": {
                  "hostname": {
                    "type": "string",
                    "maxLength": 253,
                    "description": "A subdomain on your domain",
                    "example": "vod.example.co.il"
                  }
                }
              },
              "example": {
                "hostname": "vod.example.co.il"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "All custom domains of the site after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteDomains"
                },
                "example": {
                  "items": [
                    {
                      "hostname": "vod.example.co.il",
                      "status": "pending",
                      "cf_id": "5f1c2d3e4a5b6c7d8e9f0a1b2c3d4e5f",
                      "cname_target": "sites-origin.viewstream.co.il",
                      "txt": [
                        {
                          "type": "TXT",
                          "name": "_cf-custom-hostname.vod.example.co.il",
                          "value": "4a1b2c3d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                        }
                      ],
                      "created_at": "2026-10-06T08:15:00Z",
                      "checked_at": "2026-10-06T08:15:00Z"
                    }
                  ],
                  "cname_target": "sites-origin.viewstream.co.il",
                  "saas_configured": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:admin"
      }
    },
    "/v1/sites/{id}/domains/{hostname}": {
      "delete": {
        "tags": [
          "sites"
        ],
        "summary": "Remove a custom domain (and its Cloudflare custom hostname)",
        "description": "**Required scope:** `sites:admin`\n\nRemoves the custom domain from the site and its hostnames, deleting the Cloudflare custom hostname first when\none was provisioned (a Cloudflare failure answers 502 and changes nothing). Purges the site cache; audited\n(`site.domain_removed`). Needs `sites:admin`.",
        "operationId": "deleteSiteDomain",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "hostname",
            "in": "path",
            "required": true,
            "description": "The custom domain (case-insensitive)",
            "schema": {
              "type": "string",
              "example": "vod.example.co.il"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "Cloudflare refused to delete the custom hostname (`internal_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:admin"
      }
    },
    "/v1/sites/{id}": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "One site",
        "description": "**Required scope:** `sites:read`\n\nThe site with its settings and `url` (https + the canonical hostname). Needs `sites:read`.",
        "operationId": "getSite",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                },
                "example": {
                  "id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a51",
                  "slug": "tv10poc",
                  "name": "ערוץ 10 כלכלה",
                  "hostnames": [
                    "tv10poc.viewstream.co.il"
                  ],
                  "locales": [
                    "he",
                    "en"
                  ],
                  "default_locale": "he",
                  "theme_id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a52",
                  "status": "live",
                  "settings": {
                    "epg": {
                      "days_back": 7,
                      "days_forward": 7,
                      "channels": {
                        "main": {
                          "publish": true
                        }
                      }
                    },
                    "accessibility": {},
                    "dictionary": {}
                  },
                  "created_at": "2026-09-28T10:00:00Z",
                  "updated_at": "2026-10-05T12:40:00Z",
                  "url": "https://tv10poc.viewstream.co.il"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "patch": {
        "tags": [
          "sites"
        ],
        "summary": "Update slug, name, hostnames, locales, default_locale, status draft|live|suspended, settings…",
        "description": "**Required scope:** `sites:admin`\n\nUpdate slug, name, hostnames, locales, default_locale, status draft|live|suspended, settings (seo, scripts, consent, accessibility, dictionary, player.config, epg {days_back, days_forward, channels {<slug>: {publish}}})\n\nChanges only the fields sent, with the create rules. `hostnames` replaces the whole list (send all of them);\n`settings` replaces the whole settings document, except `custom_domains`, which only the `/domains` routes change.\nSettings are validated: analytics ids (`G-…`, `GTM-…`), `ads.gam_network` (digits) when ads are enabled,\n`consent.policy_url` (path or https URL) and texts (he/en/ar/ru, ≤ 800 chars), `headless.origins` (≤ 20 origins\nor `*`), `video_ads.mode` preset|off|custom, `ai_crawlers` allow|block. `status: suspended` takes the site off\nthe air. Purges the whole site (`site:<id>`). Body ≤ 256 KB. Needs `sites:admin`.",
        "operationId": "patchSite",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SiteInput"
              },
              "example": {
                "status": "live",
                "settings": {
                  "epg": {
                    "days_back": 7,
                    "days_forward": 3,
                    "channels": {
                      "main": {
                        "publish": true
                      }
                    }
                  },
                  "analytics": {
                    "ga4_id": "G-AB12CD34EF"
                  },
                  "consent": {
                    "policy_url": "/privacy"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The site after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                },
                "example": {
                  "id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a51",
                  "slug": "tv10poc",
                  "name": "ערוץ 10 כלכלה",
                  "hostnames": [
                    "tv10poc.viewstream.co.il"
                  ],
                  "locales": [
                    "he",
                    "en"
                  ],
                  "default_locale": "he",
                  "theme_id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a52",
                  "status": "live",
                  "settings": {
                    "epg": {
                      "days_back": 7,
                      "days_forward": 3,
                      "channels": {
                        "main": {
                          "publish": true
                        }
                      }
                    },
                    "analytics": {
                      "ga4_id": "G-AB12CD34EF"
                    },
                    "consent": {
                      "policy_url": "/privacy"
                    }
                  },
                  "created_at": "2026-09-28T10:00:00Z",
                  "updated_at": "2026-10-06T08:15:00Z",
                  "url": "https://tv10poc.viewstream.co.il"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:admin"
      },
      "delete": {
        "tags": [
          "sites"
        ],
        "summary": "Delete a site and all its pages",
        "description": "**Required scope:** `sites:admin`\n\nDeletes the site with its pages, versions, theme, menus, routes, redirects and image library, and purges its cache (`site:<id>`). Shows, people and assets are tenant-wide and stay. Cannot be undone. Needs `sites:admin`.",
        "operationId": "deleteSite",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:admin"
      }
    },
    "/v1/sites/{id}/blocks": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "The block registry (types, allowed props, allowed source kinds) the builder and validation use",
        "description": "**Required scope:** `sites:read`\n\nThe block library page layouts may use (layout version 1), sorted by type: the allowed prop names, the data-source\nkinds the block accepts (`\"\"` = no source), whether it shows a list, hydrates in the browser (`island`), the\nplatforms it renders on, and `admin_only` for blocks only `sites:admin` may place (`html`). Unknown block types or\nprops are refused when a page is saved. Needs `sites:read`.",
        "operationId": "siteBlocks",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The block registry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "layout_version": {
                      "type": "integer",
                      "enum": [
                        1
                      ]
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteBlock"
                      }
                    }
                  }
                },
                "example": {
                  "layout_version": 1,
                  "items": [
                    {
                      "type": "hero",
                      "props": [
                        "layout",
                        "media",
                        "cta",
                        "autoplay",
                        "title",
                        "image"
                      ],
                      "sources": [
                        "manual",
                        "live_now",
                        "collection",
                        "newest",
                        "show_episodes",
                        "current"
                      ],
                      "list": false,
                      "island": true,
                      "platforms": [
                        "web",
                        "app",
                        "tv"
                      ]
                    },
                    {
                      "type": "html",
                      "props": [
                        "html",
                        "allowed_domains"
                      ],
                      "sources": [],
                      "list": false,
                      "island": false,
                      "platforms": [
                        "web"
                      ],
                      "admin_only": true
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      }
    },
    "/v1/sites/{id}/resolve": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Resolve a data source without saving (builder live preview)",
        "description": "**Required scope:** `sites:read`\n\nResolve a data source without saving (builder live preview): {source, limit?, entity?: {type, slug}} returns {items, more}\n\nResolves a block data source the way the Delivery API would, without saving anything (the builder's live preview).\n`source` is validated as the source of a `rail` block (422 when invalid). `limit` defaults to 20 and is capped at\n100 (`source.limit` wins when set); `more` says there are further items. `entity` sets the page entity for sources\nthat use `current` (e.g. the episodes of the show a template is previewed with). Body ≤ 64 KB. Needs `sites:read`.",
        "operationId": "siteResolve",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source"
                ],
                "properties": {
                  "source": {
                    "type": "object",
                    "description": "A block data source: kind manual|collection|show_episodes|tag|newest|most_watched|live_now|catchup|upcoming|people|related|search|person_items, plus the fields that kind uses (items, id, series, channel, order, limit, window, types, tags, days, q, include_untitled)",
                    "additionalProperties": true
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "entity": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "description": "Entity type: series, asset, clip, programme, channel, person, collection"
                      },
                      "slug": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "source": {
                  "kind": "newest",
                  "types": [
                    "asset"
                  ],
                  "limit": 6
                },
                "entity": null
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The resolved tiles",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteBuilderItem"
                      }
                    },
                    "more": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "type": "asset",
                      "id": "0192b0c4-11aa-7e3b-8c5d-2f1e0a9b8c71",
                      "slug": "market-close-ep1",
                      "href": "/v/market-close-ep1",
                      "title": {
                        "he": "סוגרים שוק — פרק 1"
                      },
                      "images": {
                        "poster": "https://img.viewstream.co.il/tv10poc/0192b0c4/poster.jpg"
                      },
                      "duration_s": 1520,
                      "badge": "new"
                    }
                  ],
                  "more": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      }
    },
    "/v1/sites/{id}/route": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Resolve a path as the Delivery API would (builder)",
        "description": "**Required scope:** `sites:read`\n\nResolves a site path exactly like `GET /s/v1/{site}/route`: a redirect first, then a page at that path, then the\nroute table, then the built-in routes (`/show/:slug`, `/podcast/:slug`, `/v/:slug`, `/c/:slug`, `/p/:slug`,\n`/live/:slug`, `/person/:slug`, `/category/:slug`, `/search`, `/epg`), else `not_found`. The path is normalised\n(leading `/` added, trailing `/` dropped). Note: when the path is a redirect source this lookup counts as a hit\nof that redirect. Needs `sites:read`.",
        "operationId": "siteRoute",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "path",
            "in": "query",
            "description": "Site path to resolve; default `/`",
            "schema": {
              "type": "string",
              "example": "/show/market-close"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Where the path leads",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteRouteResult"
                },
                "example": {
                  "kind": "template",
                  "page_id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a60",
                  "template_for": "show",
                  "entity": {
                    "type": "series",
                    "slug": "market-close"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      }
    },
    "/v1/sites/{id}/pages": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Pages and templates of a site",
        "description": "**Required scope:** `sites:read`\n\nLists every page (`kind: page`) and template (`kind: template`) of the site with its draft and its publishing\n`status` (version on air, newest version, unsaved draft changes, scheduled switches) — what the Studio pages\ntree shows. Ordered by kind, then path, then template_for; not paginated. Needs `sites:read`; Sites is for\nplatform tenants only (CDN-only tenants get 403 `feature_disabled`).",
        "operationId": "listPages",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The pages and templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/SitePage"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "status": {
                                "$ref": "#/components/schemas/SitePageStatus"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10",
                      "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                      "kind": "page",
                      "path": "/",
                      "template_for": null,
                      "title": {
                        "he": "ראשי",
                        "en": "Home"
                      },
                      "draft": {
                        "v": 1,
                        "sections": [
                          {
                            "id": "hero",
                            "type": "hero",
                            "props": {
                              "cta": "watch_live",
                              "media": "image",
                              "layout": "stage"
                            },
                            "source": {
                              "kind": "live_now"
                            }
                          }
                        ]
                      },
                      "draft_rev": 14,
                      "published_version_id": "01a0f411-0c3a-7e52-b1d4-7a8b9c0d1e2f",
                      "locked_by": null,
                      "locked_at": null,
                      "seo": {},
                      "created_at": "2026-09-12T08:10:00Z",
                      "updated_at": "2026-10-05T13:42:11Z",
                      "status": {
                        "live_version": 6,
                        "latest_version": 6,
                        "draft_changed": false
                      }
                    },
                    {
                      "id": "01a0e9c2-7d55-7c11-9a20-4b8c2d3e4f50",
                      "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                      "kind": "template",
                      "path": null,
                      "template_for": "show",
                      "title": {
                        "he": "תוכנית",
                        "en": "Show"
                      },
                      "draft": {
                        "v": 1,
                        "sections": [
                          {
                            "id": "header",
                            "type": "entity_header"
                          }
                        ]
                      },
                      "draft_rev": 3,
                      "published_version_id": "01a0f2a0-5b11-7c90-8d0e-1f2a3b4c5d6e",
                      "locked_by": null,
                      "locked_at": null,
                      "seo": {},
                      "created_at": "2026-09-12T08:10:00Z",
                      "updated_at": "2026-09-20T09:00:00Z",
                      "status": {
                        "live_version": 2,
                        "latest_version": 2,
                        "draft_changed": true
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:read"
      },
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Create a page (kind page + path) or template (kind template + template_for); draft is a layout document (v 1)",
        "description": "**Required scope:** `sites:write`\n\nCreates a page served at `path` (`kind: page`, the default) or a template that renders every entity of one type\n(`kind: template` + `template_for`). `path` starts with `/`, ≤ 200 characters, no trailing `/` (except `/`), no\n`?`, `#` or spaces, and not under `/_s/` or `/__preview`. A missing `draft` becomes an empty layout\n`{\"v\":1,\"sections\":[]}`; a given one is validated against the block registry (`html` blocks need `sites:admin`).\nNothing is public until `POST /v1/pages/{id}/publish`. Answers 201 with the page and `ETag: <draft_rev>`; 409\nwhen the path (or the template of that type) already exists. Body ≤ 2 MB, unknown fields are refused (400).",
        "operationId": "createPage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "page",
                      "template"
                    ],
                    "default": "page"
                  },
                  "path": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "kind page: required; ignored for templates"
                  },
                  "template_for": {
                    "type": "string",
                    "enum": [
                      "show",
                      "asset",
                      "clip",
                      "programme",
                      "channel",
                      "person",
                      "collection",
                      "podcast",
                      "search",
                      "epg",
                      "not_found"
                    ],
                    "description": "kind template: required; ignored for pages"
                  },
                  "title": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "per-locale title"
                  },
                  "draft": {
                    "$ref": "#/components/schemas/SitePageLayout"
                  },
                  "seo": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              },
              "example": {
                "kind": "page",
                "path": "/shows",
                "title": {
                  "he": "תוכניות",
                  "en": "Shows"
                },
                "draft": {
                  "v": 1,
                  "sections": [
                    {
                      "id": "all",
                      "type": "grid",
                      "props": {
                        "title": {
                          "he": "כל התוכניות",
                          "en": "All shows"
                        },
                        "columns": 4
                      },
                      "source": {
                        "kind": "newest",
                        "types": [
                          "asset"
                        ]
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created; `ETag` carries draft_rev",
            "headers": {
              "ETag": {
                "description": "draft_rev (send it back as If-Match when saving)",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitePage"
                },
                "example": {
                  "id": "01a0e9c2-7e02-7a14-b3c5-6d7e8f9a0b1c",
                  "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                  "kind": "page",
                  "path": "/shows",
                  "template_for": null,
                  "title": {
                    "he": "תוכניות",
                    "en": "Shows"
                  },
                  "draft": {
                    "v": 1,
                    "sections": [
                      {
                        "id": "all",
                        "type": "grid",
                        "props": {
                          "title": {
                            "he": "כל התוכניות",
                            "en": "All shows"
                          },
                          "columns": 4
                        },
                        "source": {
                          "kind": "newest",
                          "types": [
                            "asset"
                          ]
                        }
                      }
                    ]
                  },
                  "draft_rev": 1,
                  "published_version_id": null,
                  "locked_by": null,
                  "locked_at": null,
                  "seo": {},
                  "created_at": "2026-10-06T07:30:00Z",
                  "updated_at": "2026-10-06T07:30:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/sites/{id}/theme": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Theme (draft tokens + published tokens)",
        "description": "**Required scope:** `sites:read`\n\nThe site's theme — the draft `tokens` and `custom_css` being edited, and the `published_tokens` the site serves (`published_version` counts publishes; null = never published). Needs `sites:read`.",
        "operationId": "getTheme",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The theme",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteTheme"
                },
                "example": {
                  "id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a52",
                  "site_id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a51",
                  "name": "default",
                  "tokens": {
                    "colors": {
                      "bg": "#0b1b3a",
                      "fg": "#ffffff",
                      "accent": "#2ee6a8",
                      "accent_fg": "#06122a",
                      "surface": "#10244a",
                      "surface_2": "#16305e",
                      "fg_muted": "#b8c4de",
                      "live": "#e5213a",
                      "focus": "#ffd23f"
                    },
                    "fonts": {
                      "body": "Heebo",
                      "heading": "Heebo"
                    },
                    "radius": 8
                  },
                  "custom_css": null,
                  "published_version": 3,
                  "published_tokens": {
                    "colors": {
                      "bg": "#0b1b3a",
                      "fg": "#ffffff",
                      "accent": "#2ee6a8"
                    },
                    "fonts": {
                      "body": "Heebo",
                      "heading": "Heebo"
                    },
                    "radius": 8
                  },
                  "updated_at": "2026-10-04T09:30:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "put": {
        "tags": [
          "sites"
        ],
        "summary": "Save the draft theme: tokens, custom_css (32 KB max, linted; sites:admin)",
        "description": "**Required scope:** `sites:admin`\n\nReplaces the draft theme; the live site does not change until `POST /theme/publish`. `tokens` is required and must\nbe an object (colors, fonts, radius, logo, …). Every token the renderers write into CSS is validated (422 with a\nfield error per bad token, e.g. `tokens.colors.bg`): colours `#rrggbb` or `#rrggbbaa` under lowercase names\n(`[a-z][a-z0-9_]*`, at most 32); font families letters, digits, spaces, commas, hyphens and balanced quotes,\n≤ 200 characters; `radius` a number (pixels) or a length in `px`, `rem`, `em` or `%`; `style` `standard` or\n`broadcast`; `logo.light`/`logo.dark` an http(s) URL or a `/path`; `logo.width`/`height` 0–10000; `logo.alt`\n≤ 300 characters. Empty strings and null mean \"unset\". `custom_css` is linted: ≤ 32 KB, no `@import`, no remote\n`url(http…)`, no `expression` (422); omitting it or sending null clears the draft CSS. Body ≤ 128 KB. Needs\n`sites:admin`.",
        "operationId": "putTheme",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tokens"
                ],
                "properties": {
                  "tokens": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Design tokens: colors {bg, surface, surface_2, fg, fg_muted, accent, accent_fg, live, focus}, fonts {body, heading}, radius, logo"
                  },
                  "custom_css": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 32768
                  }
                }
              },
              "example": {
                "tokens": {
                  "colors": {
                    "bg": "#0b1b3a",
                    "fg": "#ffffff",
                    "accent": "#2ee6a8",
                    "accent_fg": "#06122a"
                  },
                  "fonts": {
                    "body": "Heebo",
                    "heading": "Heebo"
                  },
                  "radius": 8
                },
                "custom_css": ".vs-hero h1 { letter-spacing: -0.01em; }"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The theme after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteTheme"
                },
                "example": {
                  "id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a52",
                  "site_id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a51",
                  "name": "default",
                  "tokens": {
                    "colors": {
                      "bg": "#0b1b3a",
                      "fg": "#ffffff",
                      "accent": "#2ee6a8",
                      "accent_fg": "#06122a"
                    },
                    "fonts": {
                      "body": "Heebo",
                      "heading": "Heebo"
                    },
                    "radius": 8
                  },
                  "custom_css": ".vs-hero h1 { letter-spacing: -0.01em; }",
                  "published_version": 3,
                  "published_tokens": {
                    "colors": {
                      "bg": "#0b1b3a",
                      "fg": "#ffffff",
                      "accent": "#2ee6a8"
                    }
                  },
                  "updated_at": "2026-10-06T08:15:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:admin"
      }
    },
    "/v1/sites/{id}/theme/publish": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Publish the theme (purges site:<id>)",
        "description": "**Required scope:** `sites:publish`\n\nCopies the draft tokens and CSS to the published theme and increments `published_version`. A draft below WCAG AA\n(4.5:1) on the checked colour pairs (see `GET /theme/contrast`) is refused with 422, one `errors[]` entry per\nfailing pair. Purges the whole site (`site:<id>`); audited (`site.theme_published`). Needs `sites:publish`.",
        "operationId": "publishTheme",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The theme after publishing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteTheme"
                },
                "example": {
                  "id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a52",
                  "site_id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a51",
                  "name": "default",
                  "tokens": {
                    "colors": {
                      "bg": "#0b1b3a",
                      "fg": "#ffffff"
                    },
                    "radius": 8
                  },
                  "custom_css": null,
                  "published_version": 4,
                  "published_tokens": {
                    "colors": {
                      "bg": "#0b1b3a",
                      "fg": "#ffffff"
                    },
                    "radius": 8
                  },
                  "updated_at": "2026-10-06T08:15:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The draft is below WCAG AA contrast and cannot be published",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation_error",
                  "title": "Validation failed",
                  "status": 422,
                  "detail": "the theme is below WCAG AA contrast and cannot be published",
                  "request_id": "0192e0f1-0a1b-7c2d-8e3f-4a5b6c7d8e9f",
                  "errors": [
                    {
                      "field": "tokens.colors",
                      "detail": "fg_muted / bg is 3.12:1 (AA needs 4.5:1)"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:publish"
      }
    },
    "/v1/sites/{id}/menus/{slot}": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "A menu (draft + published)",
        "description": "**Required scope:** `sites:read`\n\nThe menu in a slot — draft `items` and the `published_items` the site serves (null until first published). A slot never saved answers with an empty draft. Needs `sites:read`.",
        "operationId": "getMenu",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "slot",
            "in": "path",
            "required": true,
            "description": "Menu slot (any other value answers 404)",
            "schema": {
              "type": "string",
              "enum": [
                "header_web",
                "header_mobile",
                "footer",
                "legal"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The menu",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteMenu"
                },
                "example": {
                  "slot": "footer",
                  "items": [
                    {
                      "kind": "page",
                      "href": "/accessibility",
                      "label": {
                        "he": "הצהרת נגישות",
                        "en": "Accessibility statement"
                      }
                    }
                  ],
                  "published_items": [
                    {
                      "kind": "page",
                      "href": "/accessibility",
                      "label": {
                        "he": "הצהרת נגישות",
                        "en": "Accessibility statement"
                      }
                    }
                  ],
                  "published_version": 2,
                  "updated_at": "2026-10-01T11:00:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "put": {
        "tags": [
          "sites"
        ],
        "summary": "Save the draft menu: items [{kind page|entity|url|separator|accessibility|search|logo, label L, href?, icon?}]",
        "description": "**Required scope:** `sites:write`\n\nReplaces the draft menu of the slot (the site changes on publish). At most 50 items; each needs a `kind` of\npage, entity, url, separator, accessibility, search or logo; other item fields (`label`, `href`, `icon`, …) are\nstored as sent for the renderer. Body ≤ 128 KB. Needs `sites:write`.",
        "operationId": "putMenu",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "slot",
            "in": "path",
            "required": true,
            "description": "Menu slot",
            "schema": {
              "type": "string",
              "enum": [
                "header_web",
                "header_mobile",
                "footer",
                "legal"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "$ref": "#/components/schemas/SiteMenuItem"
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "kind": "logo",
                    "href": "/"
                  },
                  {
                    "kind": "page",
                    "href": "/",
                    "icon": "home",
                    "label": {
                      "he": "בית",
                      "en": "Home"
                    }
                  },
                  {
                    "kind": "page",
                    "href": "/epg",
                    "icon": "guide",
                    "label": {
                      "he": "לוח שידורים",
                      "en": "TV guide"
                    }
                  },
                  {
                    "kind": "search"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The menu after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteMenu"
                },
                "example": {
                  "slot": "header_web",
                  "items": [
                    {
                      "kind": "logo",
                      "href": "/"
                    },
                    {
                      "kind": "page",
                      "href": "/",
                      "icon": "home",
                      "label": {
                        "he": "בית",
                        "en": "Home"
                      }
                    },
                    {
                      "kind": "page",
                      "href": "/epg",
                      "icon": "guide",
                      "label": {
                        "he": "לוח שידורים",
                        "en": "TV guide"
                      }
                    },
                    {
                      "kind": "search"
                    }
                  ],
                  "published_items": null,
                  "published_version": null,
                  "updated_at": "2026-10-06T08:15:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/sites/{id}/menus/{slot}/publish": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Publish the menu",
        "description": "**Required scope:** `sites:publish`\n\nCopies the draft items to the published menu and increments `published_version` (a slot never saved publishes an empty menu). Purges the whole site (`site:<id>`); audited (`site.menu_published`). Needs `sites:publish`.",
        "operationId": "publishMenu",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "slot",
            "in": "path",
            "required": true,
            "description": "Menu slot",
            "schema": {
              "type": "string",
              "enum": [
                "header_web",
                "header_mobile",
                "footer",
                "legal"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The menu after publishing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteMenu"
                },
                "example": {
                  "slot": "header_web",
                  "items": [
                    {
                      "kind": "page",
                      "href": "/",
                      "icon": "home",
                      "label": {
                        "he": "בית",
                        "en": "Home"
                      }
                    }
                  ],
                  "published_items": [
                    {
                      "kind": "page",
                      "href": "/",
                      "icon": "home",
                      "label": {
                        "he": "בית",
                        "en": "Home"
                      }
                    }
                  ],
                  "published_version": 1,
                  "updated_at": "2026-10-06T08:15:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:publish"
      }
    },
    "/v1/sites/{id}/routes": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Route table (patterns like /show/:slug to a template or page id); built-in routes apply after it",
        "description": "**Required scope:** `sites:read`\n\nThe site's own route table, highest `priority` first. A route maps a pattern (`:param` segments) to a template\nkind or a page id. Paths are resolved as: redirect, page at that exact path, this table, then the built-in routes\n(`/show/:slug`, `/podcast/:slug`, `/v/:slug`, `/c/:slug`, `/p/:slug`, `/live/:slug`, `/person/:slug`,\n`/category/:slug`, `/search`, `/epg`). Needs `sites:read`.",
        "operationId": "getRoutes",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The route table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteRoute"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "pattern": "/tochniot/:slug",
                      "target": "show",
                      "priority": 20
                    },
                    {
                      "pattern": "/about",
                      "target": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a70",
                      "priority": 0
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "put": {
        "tags": [
          "sites"
        ],
        "summary": "Replace the route table (sites:admin)",
        "description": "**Required scope:** `sites:admin`\n\nReplaces the whole route table. Each `pattern` must start with `/`, be ≤ 200 chars, contain no `?`, `#` or spaces,\nnot end with `/` (except `/`) and not start with `/_s/` or `/__preview`; `target` is a page id or a template kind\n(show, asset, clip, programme, channel, person, collection, podcast, search, epg, not_found). A pattern listed twice\nis 409. Answers like GET. Purges the whole site; audited (`site.routes_changed`). Body ≤ 256 KB. Needs `sites:admin`.",
        "operationId": "putRoutes",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/SiteRoute"
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "pattern": "/tochniot/:slug",
                    "target": "show",
                    "priority": 20
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The route table after the change",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteRoute"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "pattern": "/tochniot/:slug",
                      "target": "show",
                      "priority": 20
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:admin"
      }
    },
    "/v1/sites/{id}/redirects": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Redirects with hit counts",
        "description": "**Required scope:** `sites:read`\n\nAll redirects of the site ordered by `from_path`, with `source` (`manual` or `import`) and `hits` (how often the Delivery API followed each one). Needs `sites:read`.",
        "operationId": "getRedirects",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The redirects",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteRedirect"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "from_path": "/old-shows/market",
                      "to_path": "/show/market-close",
                      "code": 301,
                      "source": "import",
                      "hits": 128
                    },
                    {
                      "from_path": "/promo",
                      "to_path": "https://www.example.co.il/promo",
                      "code": 302,
                      "source": "manual",
                      "hits": 4
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "put": {
        "tags": [
          "sites"
        ],
        "summary": "Replace the redirects: items [{from_path, to_path, code 301|302|307|308}]",
        "description": "**Required scope:** `sites:write`\n\nReplaces ALL redirects of the site, imported ones included: send the full list (each item keeps the `source` you\nsend, default `manual`; hit counters restart at 0). `from_path` follows the route path rules (starts with `/`,\n≤ 200 chars, no `?`, `#`, spaces or trailing `/`); `to_path` is a site path or an absolute URL; `code` defaults to\n301. A `from_path` listed twice is 409. Answers like GET. Purges the whole site; audited\n(`site.redirects_changed`). Body ≤ 2 MB. Needs `sites:write`.",
        "operationId": "putRedirects",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/SiteRedirect"
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "from_path": "/old-shows/market",
                    "to_path": "/show/market-close",
                    "code": 301,
                    "source": "import"
                  },
                  {
                    "from_path": "/promo",
                    "to_path": "https://www.example.co.il/promo",
                    "code": 302
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The redirects after the change",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteRedirect"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "from_path": "/old-shows/market",
                      "to_path": "/show/market-close",
                      "code": 301,
                      "source": "import",
                      "hits": 0
                    },
                    {
                      "from_path": "/promo",
                      "to_path": "https://www.example.co.il/promo",
                      "code": 302,
                      "source": "manual",
                      "hits": 0
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/sites/{id}/redirects/import": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "bulk-import redirects from CSV (old URL, new URL[, code])",
        "description": "**Required scope:** `sites:write`\n\nbulk-import redirects from CSV (old URL, new URL[, code]); loop and chain analysis, dry_run, mode merge|replace_imported, collapse_chains\n\nImports redirects from CSV text (`old,new[,code]` per row; a header row, `#` comments and pasted TSV are accepted;\nan absolute old URL keeps only its path). At most 20,000 rows. Rows are merged over the existing redirects and\nanalysed: loops are skipped (error issues), chains are reported or, with `collapse_chains`, rewritten to one hop;\na page at a `from` path gets a warning (the redirect wins). `mode: replace_imported` first removes earlier imported\nrows; manual redirects are never changed by an import. `dry_run` only reports. `items` previews at most 500 rows.\nPurges the site and is audited (`site.redirects_imported`) unless dry run. Body ≤ 4 MB. Needs `sites:write`.",
        "operationId": "importRedirects",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "csv"
                ],
                "properties": {
                  "csv": {
                    "type": "string",
                    "description": "rows of old,new[,code]; a header row is skipped; old may be a full URL of the previous site"
                  },
                  "default_code": {
                    "type": "integer",
                    "enum": [
                      301,
                      302,
                      307,
                      308
                    ],
                    "default": 301,
                    "description": "Code of rows without one"
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "merge",
                      "replace_imported"
                    ],
                    "default": "merge"
                  },
                  "collapse_chains": {
                    "type": "boolean",
                    "default": false,
                    "description": "Rewrite chains to point at their final target"
                  },
                  "dry_run": {
                    "type": "boolean",
                    "default": false,
                    "description": "Analyse and report without saving"
                  }
                }
              },
              "example": {
                "csv": "old,new,code\nhttps://old.example.co.il/shows/market,/show/market-close,301\n/old-shows/next,/show/the-next-thing\n",
                "mode": "merge",
                "collapse_chains": true,
                "dry_run": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteRedirectImport"
                },
                "example": {
                  "dry_run": true,
                  "rows": 2,
                  "valid": 2,
                  "added": 0,
                  "updated": 0,
                  "skipped": 0,
                  "items": [
                    {
                      "from_path": "/shows/market",
                      "to_path": "/show/market-close",
                      "code": 301,
                      "hits": 0
                    },
                    {
                      "from_path": "/old-shows/next",
                      "to_path": "/show/the-next-thing",
                      "code": 301,
                      "hits": 0
                    }
                  ],
                  "issues": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/sites/{id}/redirects/test": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "follow the site's redirects from a path (chain, final URL, loop) and resolve where it lands",
        "description": "**Required scope:** `sites:read`\n\nFollows the site's saved redirects from `path` (an absolute URL keeps only its path; up to 10 hops) and reports the\nchain: `status` no_redirect, ok (one hop), chain, loop or too_long. When the chain ends on a site path (not an\nexternal URL, no loop), `route` says what that path resolves to. Body ≤ 16 KB. Needs `sites:read`.",
        "operationId": "testRedirect",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "path"
                ],
                "properties": {
                  "path": {
                    "type": "string",
                    "description": "Site path or a full URL of the old site"
                  }
                }
              },
              "example": {
                "path": "https://old.example.co.il/shows/market"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The redirect chain and where it lands",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chain": {
                      "$ref": "#/components/schemas/SiteRedirectChain"
                    },
                    "route": {
                      "$ref": "#/components/schemas/SiteRouteResult"
                    }
                  }
                },
                "example": {
                  "chain": {
                    "path": "/shows/market",
                    "hops": [
                      {
                        "from_path": "/shows/market",
                        "to_path": "/show/market-close",
                        "code": 301,
                        "source": "import",
                        "hits": 128
                      }
                    ],
                    "final": "/show/market-close",
                    "status": "ok"
                  },
                  "route": {
                    "kind": "template",
                    "page_id": "0192a0b1-2c3d-7e4f-8a9b-0c1d2e3f4a60",
                    "template_for": "show",
                    "entity": {
                      "type": "series",
                      "slug": "market-close"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`path` is missing or the body is not valid JSON (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      }
    },
    "/v1/sites/{id}/images": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Site image library: declare an upload (JPEG, PNG, WebP or AVIF, ≤ 15 MB)",
        "description": "**Required scope:** `sites:write`\n\nSite image library: declare an upload (JPEG, PNG, WebP or AVIF, ≤ 15 MB). Returns the image and where to send the file: `upload.url` (same-origin PUT, browsers) or `upload.presigned_url` (in-network tools, then POST /v1/images/{id}/complete). An image_ingest job then auto-rotates it, converts it to sRGB JPEG (PNG with alpha), strips all metadata (EXIF, GPS, XMP, IPTC, ICC) and caps it at 4096 px.\n\nStep 1 of an upload: declares the file and creates the image in status `uploading`. Send exactly\n`size_bytes` bytes of the declared type with `PUT upload.url` (`/v1/images/{id}/content`, streams through the API),\nor — from inside our network — `PUT upload.presigned_url` (valid 30 min) followed by\n`POST /v1/images/{id}/complete`. Limits: JPEG, PNG, WebP or AVIF; 1 byte to 15 MB; `usage: player_logo` ≤ 512 KB;\n`alt` per locale (he, en, ar, ru), ≤ 250 characters each. Writes the audit entry `site_image.create`.\nBody ≤ 8 KB, unknown fields refused (400). Needs `sites:write`.",
        "operationId": "createSiteImage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "content_type",
                  "size_bytes"
                ],
                "properties": {
                  "filename": {
                    "type": "string",
                    "description": "original file name (cleaned; must not end up empty)"
                  },
                  "content_type": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/webp",
                      "image/avif"
                    ]
                  },
                  "size_bytes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 15728640
                  },
                  "usage": {
                    "type": "string",
                    "enum": [
                      "hero",
                      "tile",
                      "logo",
                      "player_logo",
                      "other"
                    ],
                    "description": "player_logo: the logo over the video of a player config — at most 512 KB (524288)"
                  },
                  "alt": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 250
                    },
                    "description": "alternative text per locale: he, en, ar, ru"
                  }
                }
              },
              "example": {
                "filename": "boker-calcali-hero.jpg",
                "content_type": "image/jpeg",
                "size_bytes": 2483120,
                "usage": "hero",
                "alt": {
                  "he": "מגישי בוקר כלכלי באולפן",
                  "en": "The morning show hosts in the studio"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Declared; upload the file next",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "image": {
                      "$ref": "#/components/schemas/SiteImage"
                    },
                    "upload": {
                      "$ref": "#/components/schemas/SiteImageUpload"
                    }
                  }
                },
                "example": {
                  "image": {
                    "id": "01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2",
                    "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                    "status": "uploading",
                    "filename": "boker-calcali-hero.jpg",
                    "content_type": "image/jpeg",
                    "size_bytes": 2483120,
                    "focal": {
                      "x": 0.5,
                      "y": 0.5
                    },
                    "alt": {
                      "he": "מגישי בוקר כלכלי באולפן",
                      "en": "The morning show hosts in the studio"
                    },
                    "usage": "hero",
                    "created_by": "user:editor@example.co.il",
                    "created_at": "2026-10-06T08:10:00Z",
                    "updated_at": "2026-10-06T08:10:00Z"
                  },
                  "upload": {
                    "method": "PUT",
                    "url": "/v1/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2/content",
                    "headers": {
                      "Content-Type": "image/jpeg"
                    },
                    "max_bytes": 15728640,
                    "expires_at": "2026-10-06T08:40:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites or the image library is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      },
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "The site's image library, newest first (failed uploads included with their error)",
        "description": "**Required scope:** `sites:read`\n\nLists the site's library images, newest first, at most 200 (no pagination). Each image carries its state; ready\nimages also carry the CDN `url`, a `preview` and the focal-point `crops`. Needs `sites:read`.",
        "operationId": "listSiteImages",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Images",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "maxItems": 200,
                      "items": {
                        "$ref": "#/components/schemas/SiteImage"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2",
                      "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                      "status": "ready",
                      "filename": "boker-calcali-hero.jpg",
                      "content_type": "image/jpeg",
                      "size_bytes": 2483120,
                      "path": "/vod/tv10poc/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2-3fa9c1d2e4b5.jpg",
                      "width": 3840,
                      "height": 2160,
                      "bytes": 1120455,
                      "focal": {
                        "x": 0.42,
                        "y": 0.35
                      },
                      "alt": {
                        "he": "מגישי בוקר כלכלי באולפן"
                      },
                      "usage": "hero",
                      "job_id": "01a0f3d8-3a01-7b22-9c44-6e7f8091a2b3",
                      "created_by": "user:editor@example.co.il",
                      "created_at": "2026-10-06T08:10:00Z",
                      "updated_at": "2026-10-06T08:10:09Z",
                      "url": "https://cdn.tv10poc.vustream.net/vod/tv10poc/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2-3fa9c1d2e4b5.jpg",
                      "preview": "https://img.viewstream.co.il/sig/rs:fit:1280:1280/plain/…",
                      "crops": {
                        "16x9": "https://img.viewstream.co.il/sig/rs:fill:640:360/g:fp:0.42:0.35/plain/…",
                        "1x1": "https://img.viewstream.co.il/sig/rs:fill:600:600/g:fp:0.42:0.35/plain/…",
                        "2x3": "https://img.viewstream.co.il/sig/rs:fill:400:600/g:fp:0.42:0.35/plain/…"
                      }
                    },
                    {
                      "id": "01a0f3c1-77d2-7a10-8b21-3c4d5e6f7081",
                      "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                      "status": "failed",
                      "filename": "logo.webp",
                      "content_type": "image/webp",
                      "size_bytes": 40211,
                      "focal": {
                        "x": 0.5,
                        "y": 0.5
                      },
                      "alt": {},
                      "usage": "logo",
                      "error": "the file is not a webp image (detected: \"image/png\")",
                      "created_at": "2026-10-05T16:02:00Z",
                      "updated_at": "2026-10-05T16:02:03Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites or the image library is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:read"
      }
    },
    "/v1/images/{id}": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "One library image: state, focal point, the normalised file's URL, a preview and the 16:9 / 1:1 / 2:3 crops…",
        "description": "**Required scope:** `sites:read`\n\nOne library image: state, focal point, the normalised file's URL, a preview and the 16:9 / 1:1 / 2:3 crops at its focal point\n\nOne image of the tenant's site libraries. Poll it after an upload until `status` is `ready` (or `failed`, with\n`error`). `url`, `preview` and `crops` appear once the file is normalised (`preview`/`crops` need imgproxy).\nNeeds `sites:read`.",
        "operationId": "getSiteImage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "image id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Image",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteImage"
                },
                "example": {
                  "id": "01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2",
                  "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                  "status": "ready",
                  "filename": "boker-calcali-hero.jpg",
                  "content_type": "image/jpeg",
                  "size_bytes": 2483120,
                  "path": "/vod/tv10poc/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2-3fa9c1d2e4b5.jpg",
                  "width": 3840,
                  "height": 2160,
                  "bytes": 1120455,
                  "focal": {
                    "x": 0.42,
                    "y": 0.35
                  },
                  "alt": {
                    "he": "מגישי בוקר כלכלי באולפן"
                  },
                  "usage": "hero",
                  "ingest": {
                    "source": {
                      "format": "jpeg",
                      "exif": true,
                      "gps": true,
                      "orientation": 6
                    },
                    "output": {
                      "format": "jpeg"
                    },
                    "elapsed_ms": 840
                  },
                  "job_id": "01a0f3d8-3a01-7b22-9c44-6e7f8091a2b3",
                  "created_by": "user:editor@example.co.il",
                  "created_at": "2026-10-06T08:10:00Z",
                  "updated_at": "2026-10-06T08:10:09Z",
                  "url": "https://cdn.tv10poc.vustream.net/vod/tv10poc/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2-3fa9c1d2e4b5.jpg",
                  "preview": "https://img.viewstream.co.il/sig/rs:fit:1280:1280/plain/…",
                  "crops": {
                    "16x9": "https://img.viewstream.co.il/sig/rs:fill:640:360/g:fp:0.42:0.35/plain/…",
                    "1x1": "https://img.viewstream.co.il/sig/rs:fill:600:600/g:fp:0.42:0.35/plain/…",
                    "2x3": "https://img.viewstream.co.il/sig/rs:fill:400:600/g:fp:0.42:0.35/plain/…"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites or the image library is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:read"
      },
      "patch": {
        "tags": [
          "sites"
        ],
        "summary": "Set the focal point (x, y from 0 to 1; 0,0 = top left) used by every crop of the image (tiles, heroes)…",
        "description": "**Required scope:** `sites:write`\n\nSet the focal point (x, y from 0 to 1; 0,0 = top left) used by every crop of the image (tiles, heroes), and/or its alternative text\n\nSets the focal point every crop of the image centres on, and/or replaces its alternative text (he, en, ar, ru;\n≤ 250 characters each). When the point of a ready image moves, the crops at the old point are purged from\nCloudflare and `site:<id>` is purged from the Delivery API cache (pages embed the crop URLs). Writes the audit\nentry `site_image.update`. Body ≤ 8 KB, unknown fields refused. Needs `sites:write`.",
        "operationId": "patchSiteImage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "image id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "focal": {
                    "type": "object",
                    "required": [
                      "x",
                      "y"
                    ],
                    "properties": {
                      "x": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 1
                      },
                      "y": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 1
                      }
                    }
                  },
                  "alt": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 250
                    },
                    "description": "per locale: he, en, ar, ru"
                  }
                }
              },
              "example": {
                "focal": {
                  "x": 0.42,
                  "y": 0.35
                },
                "alt": {
                  "he": "מגישי בוקר כלכלי באולפן",
                  "en": "The morning show hosts in the studio"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The image after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteImage"
                },
                "example": {
                  "id": "01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2",
                  "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                  "status": "ready",
                  "filename": "boker-calcali-hero.jpg",
                  "content_type": "image/jpeg",
                  "size_bytes": 2483120,
                  "path": "/vod/tv10poc/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2-3fa9c1d2e4b5.jpg",
                  "width": 3840,
                  "height": 2160,
                  "bytes": 1120455,
                  "focal": {
                    "x": 0.42,
                    "y": 0.35
                  },
                  "alt": {
                    "he": "מגישי בוקר כלכלי באולפן",
                    "en": "The morning show hosts in the studio"
                  },
                  "usage": "hero",
                  "created_at": "2026-10-06T08:10:00Z",
                  "updated_at": "2026-10-06T08:20:31Z",
                  "url": "https://cdn.tv10poc.vustream.net/vod/tv10poc/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2-3fa9c1d2e4b5.jpg"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites or the image library is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      },
      "delete": {
        "tags": [
          "sites"
        ],
        "summary": "Delete a library image and its files (pages that use it show no picture there) and purge it",
        "description": "**Required scope:** `sites:write`\n\nDelete a library image and its files (pages that use it show no picture there) and purge it: the file and its AI variant on the edges, every imgproxy URL of it in Cloudflare (when the Cloudflare purge is configured)\n\nDeletes the image and its files. Pages, shows or themes that reference its `path` show no picture there. A\nready image is purged from the caches: an edge purge job for the file and its AI-upscaled variant, and every\nimgproxy URL of both in Cloudflare (`cloudflare: disabled` when no Cloudflare purge is configured). Writes the\naudit entry `site_image.delete`. Not reversible. Needs `sites:write`.",
        "operationId": "deleteSiteImage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "image id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted, with what was purged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "purge": {
                      "$ref": "#/components/schemas/SiteImagePurge"
                    }
                  }
                },
                "example": {
                  "deleted": "01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2",
                  "purge": {
                    "edge_keys": [
                      "cdn.tv10poc.vustream.net/vod/tv10poc/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2-3fa9c1d2e4b5.jpg",
                      "cdn.tv10poc.vustream.net/vod/tv10poc/images/01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2-3fa9c1d2e4b5_ai.jpg"
                    ],
                    "cloudflare_urls": [
                      "https://img.viewstream.co.il/sig/rs:fill:640:360/g:sm/plain/…"
                    ],
                    "cloudflare": "queued"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites or the image library is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/images/{id}/content": {
      "put": {
        "tags": [
          "sites"
        ],
        "summary": "Upload the declared file (exactly size_bytes, ≤ 15 MB; the bytes must be the declared type)",
        "description": "**Required scope:** `sites:write`\n\nUpload the declared file (exactly size_bytes, ≤ 15 MB; the bytes must be the declared type). Streams through the API (browsers cannot reach object storage) and queues image_ingest.\n\nStep 2 of an upload: send the raw bytes with `Content-Type` = the declared type and a `Content-Length` equal to\n`size_bytes` (chunked uploads without a length are refused with 413). The bytes are sniffed and must be the\ndeclared format. Allowed while the image is `uploading` or `failed` (a retry); then the image_ingest job is\nqueued (status `processing`) — poll `GET /v1/images/{id}` until `ready`. Writes the audit entry\n`site_image.upload`. Needs `sites:write`.",
        "operationId": "putSiteImageContent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "image id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/avif": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Uploaded; processing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteImage"
                },
                "example": {
                  "id": "01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2",
                  "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                  "status": "processing",
                  "filename": "boker-calcali-hero.jpg",
                  "content_type": "image/jpeg",
                  "size_bytes": 2483120,
                  "focal": {
                    "x": 0.5,
                    "y": 0.5
                  },
                  "alt": {
                    "he": "מגישי בוקר כלכלי באולפן"
                  },
                  "usage": "hero",
                  "job_id": "01a0f3d8-3a01-7b22-9c44-6e7f8091a2b3",
                  "created_by": "user:editor@example.co.il",
                  "created_at": "2026-10-06T08:10:00Z",
                  "updated_at": "2026-10-06T08:10:05Z"
                }
              }
            }
          },
          "400": {
            "description": "The upload was cut short (fewer bytes than size_bytes)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Already uploaded (status processing or ready) (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "Content-Length missing, 0 or larger than 15 MB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites or the image library is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/images/{id}/complete": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "After a PUT to upload.presigned_url (in-network tools): check the object and queue image_ingest. Idempotent.",
        "description": "**Required scope:** `sites:write`\n\nStep 2b for in-network tools that PUT the file to `upload.presigned_url`: checks the uploaded object (it must\nexist and be 1 byte to 15 MB) and queues image_ingest (status `processing`). Idempotent: an image already\n`processing` or `ready` is returned unchanged with 202; a `failed` image is re-queued. Writes the audit entry\n`site_image.upload`. No body. Needs `sites:write`.",
        "operationId": "completeSiteImage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "image id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Processing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteImage"
                },
                "example": {
                  "id": "01a0f3d8-2c6e-7f41-a8b3-5d6e7f8091a2",
                  "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                  "status": "processing",
                  "filename": "boker-calcali-hero.jpg",
                  "content_type": "image/jpeg",
                  "size_bytes": 2483120,
                  "focal": {
                    "x": 0.5,
                    "y": 0.5
                  },
                  "alt": {},
                  "usage": "hero",
                  "job_id": "01a0f3d8-3a01-7b22-9c44-6e7f8091a2b3",
                  "created_by": "key:ab12cd34",
                  "created_at": "2026-10-06T08:10:00Z",
                  "updated_at": "2026-10-06T08:10:05Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The image cannot be completed in its state (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was uploaded, or the object is not 1 byte to 15 MB (`validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites or the image library is not available on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/pages/{id}": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "A page with its draft; ETag = draft_rev",
        "description": "**Required scope:** `sites:read`\n\nReturns a page or template of the tenant with its current draft. The `ETag` header (and `draft_rev`) is the\nrevision to send as `If-Match` when saving. What visitors see is the published version — read it through the\nDelivery API (`GET /s/v1/{site}/pages/{id}`) or the history (`GET /v1/pages/{id}/versions`). Needs `sites:read`.",
        "operationId": "getPage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page",
            "headers": {
              "ETag": {
                "description": "draft_rev",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitePage"
                },
                "example": {
                  "id": "01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10",
                  "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                  "kind": "page",
                  "path": "/",
                  "template_for": null,
                  "title": {
                    "he": "ראשי",
                    "en": "Home"
                  },
                  "draft": {
                    "v": 1,
                    "sections": [
                      {
                        "id": "hero",
                        "type": "hero",
                        "props": {
                          "cta": "watch_live",
                          "media": "image",
                          "layout": "stage"
                        },
                        "source": {
                          "kind": "live_now"
                        }
                      },
                      {
                        "id": "shows",
                        "type": "rail",
                        "props": {
                          "title": {
                            "he": "תוכניות",
                            "en": "Shows"
                          },
                          "tile": {
                            "size": "L",
                            "aspect": "16x9",
                            "hover": "detailed"
                          }
                        },
                        "source": {
                          "kind": "manual",
                          "items": [
                            {
                              "type": "series",
                              "id": "01a0e9b6-6732-7134-8f64-bacbe3ddd891"
                            }
                          ]
                        }
                      }
                    ]
                  },
                  "draft_rev": 14,
                  "published_version_id": "01a0f411-0c3a-7e52-b1d4-7a8b9c0d1e2f",
                  "locked_by": null,
                  "locked_at": null,
                  "seo": {},
                  "created_at": "2026-09-12T08:10:00Z",
                  "updated_at": "2026-10-05T13:42:11Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:read"
      },
      "patch": {
        "tags": [
          "sites"
        ],
        "summary": "Save the draft (draft, title, seo, path) with If-Match",
        "description": "**Required scope:** `sites:write`\n\nSave the draft (draft, title, seo, path) with If-Match: <draft_rev>; 409 on a stale rev, 423 when locked by someone else, 422 on an invalid layout\n\nSaves the draft with optimistic concurrency: `If-Match` must equal the current `draft_rev` (quotes and `W/` are\nignored), else 409 with the current rev in `ETag` — reload and merge. Fields you omit keep their value. A page\nwhose soft lock is held by another user (lock younger than 10 min) answers 423; API keys never hold a lock, so\nthey can save only while no user holds one. `draft` is validated against the block registry (`html` blocks\nneed `sites:admin`). Each save increments `draft_rev`; nothing is public until publish. Body ≤ 2 MB, unknown\nfields refused.",
        "operationId": "patchPage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "description": "the draft_rev you loaded (the ETag of GET /v1/pages/{id})",
            "schema": {
              "type": "string",
              "example": "14"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "draft": {
                    "$ref": "#/components/schemas/SitePageLayout"
                  },
                  "title": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  },
                  "seo": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "path": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "new path (pages); same rules as on create"
                  }
                }
              },
              "example": {
                "title": {
                  "he": "ראשי",
                  "en": "Home"
                },
                "draft": {
                  "v": 1,
                  "sections": [
                    {
                      "id": "hero",
                      "type": "hero",
                      "props": {
                        "cta": "watch_live",
                        "media": "image",
                        "layout": "stage"
                      },
                      "source": {
                        "kind": "live_now"
                      }
                    },
                    {
                      "id": "top",
                      "type": "rail_top10",
                      "props": {
                        "title": {
                          "he": "הנצפים ביותר",
                          "en": "Most watched"
                        }
                      },
                      "source": {
                        "kind": "most_watched",
                        "window": "7d"
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; `ETag` carries the new draft_rev",
            "headers": {
              "ETag": {
                "description": "the new draft_rev",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitePage"
                },
                "example": {
                  "id": "01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10",
                  "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                  "kind": "page",
                  "path": "/",
                  "template_for": null,
                  "title": {
                    "he": "ראשי",
                    "en": "Home"
                  },
                  "draft": {
                    "v": 1,
                    "sections": [
                      {
                        "id": "hero",
                        "type": "hero",
                        "props": {
                          "cta": "watch_live",
                          "media": "image",
                          "layout": "stage"
                        },
                        "source": {
                          "kind": "live_now"
                        }
                      },
                      {
                        "id": "top",
                        "type": "rail_top10",
                        "props": {
                          "title": {
                            "he": "הנצפים ביותר",
                            "en": "Most watched"
                          }
                        },
                        "source": {
                          "kind": "most_watched",
                          "window": "7d"
                        }
                      }
                    ]
                  },
                  "draft_rev": 15,
                  "published_version_id": "01a0f411-0c3a-7e52-b1d4-7a8b9c0d1e2f",
                  "locked_by": "01a0d7e4-1b2c-7d3e-9f40-6a7b8c9d0e1f",
                  "locked_at": "2026-10-06T07:41:02Z",
                  "seo": {},
                  "created_at": "2026-09-12T08:10:00Z",
                  "updated_at": "2026-10-06T07:44:30Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Stale draft_rev (`conflict`); `ETag` carries the current draft_rev",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/conflict",
                  "title": "Conflict",
                  "status": 409,
                  "detail": "the draft changed since you loaded it (stale draft_rev); reload and merge",
                  "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "423": {
            "description": "Another user holds the soft lock (problem type `conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "428": {
            "description": "If-Match missing or not a number (problem type `validation_error`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      },
      "delete": {
        "tags": [
          "sites"
        ],
        "summary": "Delete a page",
        "description": "**Required scope:** `sites:write`\n\nDeletes the page or template with its draft and its whole version history, and purges `page:<id>` from the\nDelivery API cache; the Delivery API then answers 404 for it. Not reversible; there is no lock check. Needs\n`sites:write`.",
        "operationId": "deletePage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/pages/{id}/lock": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Take the soft lock (10 min; ?force=true takes it over)",
        "description": "**Required scope:** `sites:write`\n\nTakes (or renews) the soft edit lock of the page for the calling Studio user; it holds 10 minutes without\nrenewal — the editor calls this again while open. While another user's lock is fresh, saves by others answer\n423 and this call answers 423 with the holder, unless `force=true` takes it over. Locks belong to users:\nAPI keys get 403. Needs `sites:write`.",
        "operationId": "lockPage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "`true` takes over a lock held by someone else",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The lock is yours",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "locked_by": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "locked_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "ttl_s": {
                      "type": "integer",
                      "description": 600
                    }
                  }
                },
                "example": {
                  "locked_by": "01a0d7e4-1b2c-7d3e-9f40-6a7b8c9d0e1f",
                  "locked_at": "2026-10-06T07:41:02Z",
                  "ttl_s": 600
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "423": {
            "description": "Locked by another user (plain JSON, not problem+json): who holds it and since when",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "locked_by": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "locked_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "locked_by": "01a0d7e5-3c4d-7e5f-8a61-7b8c9d0e1f2a",
                  "locked_at": "2026-10-06T07:38:40Z"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      },
      "delete": {
        "tags": [
          "sites"
        ],
        "summary": "Release the soft lock",
        "description": "**Required scope:** `sites:write`\n\nClears the page's soft lock, whoever holds it (the editor calls it when it closes). Idempotent: answers 204\nalso when there is no lock or no page of the tenant with this id. Needs `sites:write`.",
        "operationId": "unlockPage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Released"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "The id is not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/pages/{id}/publish": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Publish the draft as a new version",
        "description": "**Required scope:** `sites:publish`\n\nPublish the draft as a new version: {note?, go_live_at? (scheduled), go_off_at? (takeover end: the previous version returns by itself)} (sites:publish); purges page:<id>\n\nSnapshots the current draft as the next version. Without `go_live_at` it is on air at once; with it the version\nis scheduled (≥ now − 1 min, else 422) and switches by itself. `go_off_at` (after `go_live_at`) ends a takeover:\nthe previous version returns by itself. The layout is re-validated and `site_check` runs: accessibility errors\nblock publishing (422 with the `check` report), warnings are returned in `check`. Purges `page:<id>` from the\nDelivery API cache and writes the audit entry `page.published` (or `page.scheduled`). The body is optional\n(≤ 16 KB). Needs `sites:publish`.",
        "operationId": "publishPage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "shown in the version history"
                  },
                  "go_live_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "schedule the version (omit to publish now)"
                  },
                  "go_off_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "takeover end: after it the previous version is on air again"
                  }
                }
              },
              "example": {
                "note": "Election night takeover",
                "go_live_at": "2026-10-27T17:00:00Z",
                "go_off_at": "2026-10-28T03:00:00Z"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Published (or scheduled)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "integer"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "version id"
                    },
                    "page_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "published_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "go_live_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "go_off_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "note": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "check": {
                      "$ref": "#/components/schemas/SiteCheckResult"
                    }
                  }
                },
                "example": {
                  "version": 7,
                  "id": "01a0f5b2-8e13-7a40-9c21-0d1e2f3a4b5c",
                  "page_id": "01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10",
                  "published_at": "2026-10-06T07:50:12Z",
                  "go_live_at": "2026-10-27T17:00:00Z",
                  "go_off_at": "2026-10-28T03:00:00Z",
                  "note": "Election night takeover",
                  "check": {
                    "ok": true,
                    "errors": [],
                    "warnings": [
                      {
                        "section": "shows",
                        "code": "few_items",
                        "message": "only 2 items right now"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid schedule (`go_off_at` not after `go_live_at`, `go_live_at` in the past) or an invalid layout\n(problem+json `validation_error` with `errors[]`), or site_check found accessibility errors — then the body\nis `application/json` with `title: \"site_check failed\"` and the full `check` report.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer"
                    },
                    "code": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    },
                    "check": {
                      "$ref": "#/components/schemas/SiteCheckResult"
                    }
                  }
                },
                "example": {
                  "type": "https://viewstream.co.il/problems/validation",
                  "title": "site_check failed",
                  "status": 422,
                  "code": "validation_error",
                  "detail": "publishing is blocked by accessibility errors; fix them and publish again",
                  "check": {
                    "ok": false,
                    "errors": [
                      {
                        "section": "promos",
                        "code": "a11y_alt",
                        "message": "promo tile 2 has an image without alt text"
                      }
                    ],
                    "warnings": []
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:publish"
      }
    },
    "/v1/pages/{id}/versions": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "Version history, newest first",
        "description": "**Required scope:** `sites:read`\n\nEvery published and scheduled version of the page with its layout, schedule and note, newest first (not\npaginated). Which one is on air now: `status.live_version` in `GET /v1/sites/{id}/pages`. Preview an old\nversion with the page's preview token and `&version=N` on the Delivery API. Needs `sites:read`.",
        "operationId": "listPageVersions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The versions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SitePageVersion"
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "01a0f5b2-8e13-7a40-9c21-0d1e2f3a4b5c",
                      "page_id": "01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10",
                      "version": 7,
                      "layout": {
                        "v": 1,
                        "sections": [
                          {
                            "id": "hero",
                            "type": "hero",
                            "source": {
                              "kind": "live_now"
                            }
                          }
                        ]
                      },
                      "seo": {},
                      "title": {
                        "he": "ראשי",
                        "en": "Home"
                      },
                      "published_by": "01a0d7e4-1b2c-7d3e-9f40-6a7b8c9d0e1f",
                      "published_at": "2026-10-06T07:50:12Z",
                      "go_live_at": "2026-10-27T17:00:00Z",
                      "go_off_at": "2026-10-28T03:00:00Z",
                      "note": "Election night takeover",
                      "created_at": "2026-10-06T07:50:12Z"
                    },
                    {
                      "id": "01a0f411-0c3a-7e52-b1d4-7a8b9c0d1e2f",
                      "page_id": "01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10",
                      "version": 6,
                      "layout": {
                        "v": 1,
                        "sections": [
                          {
                            "id": "hero",
                            "type": "hero",
                            "source": {
                              "kind": "live_now"
                            }
                          }
                        ]
                      },
                      "seo": {},
                      "title": {
                        "he": "ראשי",
                        "en": "Home"
                      },
                      "published_by": null,
                      "published_at": "2026-10-05T13:45:00Z",
                      "go_live_at": null,
                      "go_off_at": null,
                      "note": null,
                      "created_at": "2026-10-05T13:45:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:read"
      }
    },
    "/v1/pages/{id}/versions/{v}/restore": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Copy a version into the draft",
        "description": "**Required scope:** `sites:write`\n\nReplaces the draft's layout, title and SEO with those of version `v` and increments `draft_rev` (no If-Match\nand no lock check). Nothing changes on air until you publish again. Writes the audit entry `page.restored`.\nAnswers the page with `ETag: <draft_rev>`. Needs `sites:write`.",
        "operationId": "restorePageVersion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "v",
            "in": "path",
            "required": true,
            "description": "version number (1",
            "2": null,
            "…)": null,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page with the restored draft",
            "headers": {
              "ETag": {
                "description": "the new draft_rev",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitePage"
                },
                "example": {
                  "id": "01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10",
                  "site_id": "01a0e9b6-5f10-7a3c-9c1e-2b7d4e6f8a01",
                  "kind": "page",
                  "path": "/",
                  "template_for": null,
                  "title": {
                    "he": "ראשי",
                    "en": "Home"
                  },
                  "draft": {
                    "v": 1,
                    "sections": [
                      {
                        "id": "hero",
                        "type": "hero",
                        "source": {
                          "kind": "live_now"
                        }
                      }
                    ]
                  },
                  "draft_rev": 16,
                  "published_version_id": "01a0f411-0c3a-7e52-b1d4-7a8b9c0d1e2f",
                  "locked_by": null,
                  "locked_at": null,
                  "seo": {},
                  "created_at": "2026-09-12T08:10:00Z",
                  "updated_at": "2026-10-06T08:02:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such page or version",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/pages/{id}/preview-links": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "Signed 24 h preview: {token, expires_at, url (on the site host), api (Delivery API URL with ?preview=)}",
        "description": "**Required scope:** `sites:write`\n\nIssues an HMAC-signed preview token for the page, valid 24 hours, that shows the current draft (and, with\n`&version=N`, any of its versions) through the Delivery API — responses with it are `private, no-store` and\n`X-Robots-Tag: noindex`. `api` is the Delivery API URL; `url` opens the page on the site's own\n`<slug>.viewstream.co.il` host under `/__preview` (absent when the site has no hostname). The token is not\nstored and cannot be revoked before it expires. Needs `sites:write`.",
        "operationId": "previewLink",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "The preview link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "description": "<expiry unix seconds>.<base64url HMAC>"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "api": {
                      "type": "string",
                      "description": "GET /s/v1/{site}/pages/{id}?preview=<token>"
                    },
                    "url": {
                      "type": "string",
                      "description": "the preview on the site host (sites with a hostname only)"
                    }
                  }
                },
                "example": {
                  "token": "1759823400.Qm9ndXNTaWduYXR1cmVGb3JEb2NzMDEyMzQ1Njc4OWFi",
                  "expires_at": "2026-10-07T07:50:00Z",
                  "api": "https://api.viewstream.co.il/s/v1/tv10poc/pages/01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10?preview=1759823400.Qm9ndXNTaWduYXR1cmVGb3JEb2NzMDEyMzQ1Njc4OWFi",
                  "url": "https://tv10poc.viewstream.co.il/__preview/?page=01a0e9c2-7d41-7b20-8e55-3f9a1c2d4e10&token=1759823400.Qm9ndXNTaWduYXR1cmVGb3JEb2NzMDEyMzQ1Njc4OWFi"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/pages/{id}/check": {
      "post": {
        "tags": [
          "sites"
        ],
        "summary": "site_check: accessibility errors (block publishing), link/data/performance-budget warnings and the theme…",
        "description": "**Required scope:** `sites:write`\n\nsite_check: accessibility errors (block publishing), link/data/performance-budget warnings and the theme contrast report, for the draft or for the layout in the body\n\nRuns `site_check` — the same check publish runs — on the saved draft, or on the layout sent as `draft` (an\nunsaved editor state; nothing is saved). Errors (missing rail titles, images without alt text, an invalid\nlayout) would block publishing; warnings (broken links, empty or short sources, more than 25 sections or 4\ninteractive islands, contrast) would not. Always 200; read `ok`. Body optional, ≤ 2 MB. Needs `sites:write`.",
        "operationId": "checkPage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "draft": {
                    "$ref": "#/components/schemas/SitePageLayout"
                  }
                }
              },
              "example": {
                "draft": {
                  "v": 1,
                  "sections": [
                    {
                      "id": "shows",
                      "type": "rail",
                      "props": {
                        "title": {
                          "he": "תוכניות",
                          "en": "Shows"
                        }
                      },
                      "source": {
                        "kind": "collection",
                        "id": "01a0e9d0-4a2b-7c3d-8e4f-5a6b7c8d9e0f"
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The site_check report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteCheckResult"
                },
                "example": {
                  "ok": false,
                  "errors": [
                    {
                      "section": "promos",
                      "code": "a11y_alt",
                      "message": "promo tile 2 has an image without alt text"
                    }
                  ],
                  "warnings": [
                    {
                      "code": "budget_sections",
                      "message": "27 sections (budget 25): the page will be slow on phones"
                    }
                  ],
                  "theme": [
                    {
                      "pair": "text/background",
                      "ratio": 12.6,
                      "ok": true
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/pages/{id}/versions/{v}": {
      "delete": {
        "tags": [
          "sites"
        ],
        "summary": "Cancel a scheduled version that has not gone live yet",
        "description": "**Required scope:** `sites:publish`\n\nDeletes version `v` when it is scheduled (`go_live_at` in the future); the page keeps whatever is on air.\nA version that is already live, was published immediately or does not exist (also an unknown page) answers\n409. Purges `page:<id>`\nfrom the Delivery API cache and writes the audit entry `page.schedule_cancelled`. Needs `sites:publish`.",
        "operationId": "cancelScheduledVersion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "page id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "v",
            "in": "path",
            "required": true,
            "description": "version number",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Cancelled"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`v` is not a number or the id not a UUID",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "sites:publish"
      }
    },
    "/v1/sites/{id}/theme/contrast": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "WCAG AA contrast report of the theme draft and the published theme; publishing a theme below AA answers 422",
        "description": "**Required scope:** `sites:read`\n\nContrast ratios of the theme's text colour pairs at WCAG AA (4.5:1): fg / bg, fg / surface, fg_muted / bg,\naccent_fg / accent and white on `live` (the live badge); missing colours fall back to the renderer defaults.\n`draft_ok` is false when any draft pair fails — then `POST /theme/publish` answers 422. `published` is present once\nthe theme was published. Needs `sites:read`.",
        "operationId": "themeContrast",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Site id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The contrast report",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteContrastRow"
                      }
                    },
                    "draft_ok": {
                      "type": "boolean"
                    },
                    "published": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteContrastRow"
                      }
                    }
                  }
                },
                "example": {
                  "draft": [
                    {
                      "pair": "fg / bg",
                      "ratio": 16.4,
                      "ok": true
                    },
                    {
                      "pair": "fg / surface",
                      "ratio": 14.9,
                      "ok": true
                    },
                    {
                      "pair": "fg_muted / bg",
                      "ratio": 10.1,
                      "ok": true
                    },
                    {
                      "pair": "accent_fg / accent",
                      "ratio": 11.2,
                      "ok": true
                    },
                    {
                      "pair": "#fff / live (badge)",
                      "ratio": 4.62,
                      "ok": true
                    }
                  ],
                  "draft_ok": true,
                  "published": [
                    {
                      "pair": "fg / bg",
                      "ratio": 16.4,
                      "ok": true
                    },
                    {
                      "pair": "fg / surface",
                      "ratio": 14.9,
                      "ok": true
                    },
                    {
                      "pair": "fg_muted / bg",
                      "ratio": 10.1,
                      "ok": true
                    },
                    {
                      "pair": "accent_fg / accent",
                      "ratio": 11.2,
                      "ok": true
                    },
                    {
                      "pair": "#fff / live (badge)",
                      "ratio": 4.62,
                      "ok": true
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      }
    },
    "/v1/assets/{id}/people": {
      "get": {
        "tags": [
          "sites"
        ],
        "summary": "People appearing in an asset",
        "description": "**Required scope:** `sites:read`\n\nThe people linked to an asset of the tenant (used for person pages and JSON-LD on the sites), ordered by Hebrew name; `link_role` is the role in this asset. Needs `sites:read`.",
        "operationId": "getAssetPeople",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The people of the asset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Person"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "link_role": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c01",
                      "slug": "dana-levi",
                      "name": {
                        "he": "דנה לוי"
                      },
                      "role": {
                        "he": "מגישה"
                      },
                      "bio": {},
                      "image_key": null,
                      "links": {},
                      "link_role": "מגישה"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:read"
      },
      "put": {
        "tags": [
          "sites"
        ],
        "summary": "Replace the people of an asset: items [{person_id, role?}]",
        "description": "**Required scope:** `sites:write`\n\nReplaces the people linked to the asset. Every person must belong to the tenant (else 422); the asset must be the tenant's (else 404). Body ≤ 256 KB. Purges the Sites cache tag `entity:asset:<id>`. Needs `sites:write`.",
        "operationId": "putAssetPeople",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Asset id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "person_id"
                      ],
                      "properties": {
                        "person_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "role": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "person_id": "0192a9e0-7f3c-7b11-a2d4-5e6f7a8b9c01",
                    "role": "מגישה"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Saved"
          },
          "400": {
            "$ref": "#/components/responses/SitesBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SitesDisabled"
          }
        },
        "x-required-scope": "sites:write"
      }
    },
    "/v1/monitors/checks": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "The check catalogue (live stream, QoE, delivery) with default thresholds, units, Hebrew/English labels, and…",
        "description": "**Required scope:** `stats:read` or `channels:read`\n\nThe check catalogue (live stream, QoE, delivery) with default thresholds, units, Hebrew/English labels, and the presets\n\nEverything a monitor editor needs: every check type with its family, the target kinds it applies to, the\ncomparison (`op`), unit, default threshold / duration / window / severity and Hebrew and English labels and help;\nthe severities; and the presets (today `basic` for a channel, see `POST /v1/monitors/presets/basic`). The `lipsync`\ncheck is left out when lip-sync is turned off on the platform. Static; read with `stats:read` or `channels:read`.",
        "operationId": "listMonitorChecks",
        "responses": {
          "200": {
            "description": "Catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "checks",
                    "presets"
                  ],
                  "properties": {
                    "checks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MonitorCheckDef"
                      }
                    },
                    "severities": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "info",
                          "warning",
                          "critical"
                        ]
                      }
                    },
                    "presets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "enum": [
                              "basic"
                            ]
                          },
                          "target_kind": {
                            "type": "string",
                            "enum": [
                              "channel"
                            ]
                          },
                          "name_he": {
                            "type": "string"
                          },
                          "name_en": {
                            "type": "string"
                          },
                          "checks": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/MonitorCheck"
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "checks": [
                    {
                      "type": "feed_down",
                      "family": "live",
                      "targets": [
                        "channel"
                      ],
                      "op": "gt",
                      "unit": "s",
                      "threshold": 0,
                      "for_s": 60,
                      "window_s": 0,
                      "severity": "critical",
                      "label_he": "הפיד נפל / סלייט במסך",
                      "label_en": "Feed down / slate on",
                      "help_he": "אף מקודד לא מדווח שהפיד פעיל — הצופים רואים את הסלייט.",
                      "help_en": "No encoder reports the feed up — viewers see the slate."
                    }
                  ],
                  "severities": [
                    "info",
                    "warning",
                    "critical"
                  ],
                  "presets": [
                    {
                      "id": "basic",
                      "target_kind": "channel",
                      "name_he": "ניטור בסיסי לערוץ",
                      "name_en": "Basic channel monitoring",
                      "checks": [
                        {
                          "key": "feed_down",
                          "type": "feed_down",
                          "threshold": 0,
                          "for_s": 60,
                          "severity": "critical"
                        },
                        {
                          "key": "recorder_gap",
                          "type": "recorder_gap",
                          "threshold": 5,
                          "for_s": 0,
                          "window_s": 600,
                          "severity": "warning"
                        },
                        {
                          "key": "startup_p95",
                          "type": "startup_p95",
                          "threshold": 5000,
                          "for_s": 600,
                          "window_s": 600,
                          "severity": "warning"
                        },
                        {
                          "key": "error_rate",
                          "type": "error_rate",
                          "threshold": 5,
                          "for_s": 600,
                          "window_s": 600,
                          "severity": "critical"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "stats:read or channels:read"
      }
    },
    "/v1/monitors": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "The tenant's monitors with their evaluated status (ok, pending, firing, muted, nodata, disabled) and…",
        "description": "**Required scope:** `stats:read` or `channels:read`\n\nThe tenant's monitors with their evaluated status (ok, pending, firing, muted, nodata, disabled) and per-check state\n\nAll monitors of the tenant (at most 100) with a display label of the target, the overall `status` and the latest\nstate of every check (value, since when, the open alert). `counts` gives the number of monitors per status;\n`can_manage` tells whether the caller may change them (`notifications:manage`).",
        "operationId": "listMonitors",
        "responses": {
          "200": {
            "description": "Monitors",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Monitor"
                      }
                    },
                    "counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Monitors per status"
                    },
                    "can_manage": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "0199b3e1-4f50-7a61-8b72-9c83d4e5f601",
                      "name": "ערוץ ראשי — QoE",
                      "target_kind": "channel",
                      "target_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                      "target_label": "חדשות 10",
                      "checks": [
                        {
                          "key": "feed_down",
                          "type": "feed_down",
                          "threshold": 0,
                          "for_s": 60,
                          "severity": "critical"
                        },
                        {
                          "key": "startup_p95",
                          "type": "startup_p95",
                          "threshold": 5000,
                          "for_s": 600,
                          "window_s": 600,
                          "severity": "warning"
                        }
                      ],
                      "destinations": [
                        "0199b3e0-aa11-7b22-8c33-d44e55f66a77"
                      ],
                      "inapp": true,
                      "notify_resolve": true,
                      "repeat_min": 0,
                      "quiet_hours": {
                        "from": "23:00",
                        "to": "07:00",
                        "tz": "Asia/Jerusalem",
                        "allow_critical": true
                      },
                      "snoozed_until": null,
                      "enabled": true,
                      "created_by": "0199a3c4-2b30-7d40-9e5f-6a7b8c9d0e1f",
                      "created_at": "2026-10-05T12:00:00Z",
                      "updated_at": "2026-10-05T12:00:00Z",
                      "status": "ok",
                      "states": [
                        {
                          "check_key": "feed_down",
                          "state": "ok",
                          "since": "2026-10-05T12:01:00Z",
                          "value": 0,
                          "evaluated_at": "2026-10-06T08:14:30Z"
                        },
                        {
                          "check_key": "startup_p95",
                          "state": "ok",
                          "since": "2026-10-05T12:01:00Z",
                          "value": 1840,
                          "evaluated_at": "2026-10-06T08:14:30Z"
                        }
                      ]
                    }
                  ],
                  "counts": {
                    "ok": 1
                  },
                  "can_manage": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read or channels:read"
      },
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "Create a monitor — one target (channel, asset, site host or the whole tenant), its checks…",
        "description": "**Required scope:** `notifications:manage`\n\nCreate a monitor — one target (channel, asset, site host or the whole tenant), its checks (threshold, duration, severity), destinations, quiet hours\n\nChecks are evaluated every 30–60 s on your tenant's data only. A check fires after its condition held for `for_s`\nseconds and resolves after it read healthy for up to a minute. Alerts go to the Studio bell (`inapp`), to the\nlisted destinations (e-mail, Telegram) and as `alert.firing` / `alert.resolved` events to your webhooks.\nQuiet hours hold e-mail/Telegram (critical alerts pass with `allow_critical`); a snooze holds everything but the record.\nRules: name 1–120 chars; 1–20 checks that apply to the target kind, unique `key`s; the target and every destination\n(max 10) must belong to the tenant; `repeat_min` 0 or 30–1440; at most 100 monitors per tenant (422). Body\nlimit 64 KiB. Audits `monitor.create`. The new monitor reads `nodata` until its first evaluation.",
        "operationId": "createMonitor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonitorInput"
              },
              "example": {
                "name": "ערוץ ראשי — QoE",
                "target_kind": "channel",
                "target_id": "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2b",
                "checks": [
                  {
                    "type": "feed_down",
                    "for_s": 60,
                    "severity": "critical"
                  },
                  {
                    "type": "startup_p95",
                    "threshold": 5000,
                    "for_s": 600,
                    "window_s": 600
                  }
                ],
                "destinations": [
                  "019286a2-1f3e-7c4b-9a1b-3c5d7e9f1a2c"
                ],
                "quiet_hours": {
                  "from": "23:00",
                  "to": "07:00",
                  "tz": "Asia/Jerusalem",
                  "allow_critical": true
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/monitors/presets/basic": {
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "One-click basic monitoring of a channel",
        "description": "**Required scope:** `notifications:manage`\n\nOne-click basic monitoring of a channel — feed down 60 s, recorder gap, startup p95 > 5 s for 10 min, error rate > 5 % for 10 min\n\nCreates a monitor on the channel with the `basic` preset checks (feed down 60 s — critical; recorder gap > 5 s in\n10 min — warning; startup p95 > 5 s for 10 min — warning; error rate > 5 % for 10 min — critical), in-app and\nresolve notifications on. `name` defaults to \"ניטור בסיסי — <channel title>\". Same rules and limits as\n`POST /v1/monitors`. Body limit 16 KiB. Audits `monitor.create`.",
        "operationId": "createBasicMonitor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "channel_id"
                ],
                "properties": {
                  "channel_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "A channel of the tenant"
                  },
                  "destinations": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Alert destination ids"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  }
                }
              },
              "example": {
                "channel_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                "destinations": [
                  "0199b3e0-aa11-7b22-8c33-d44e55f66a77"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/monitors/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "One monitor with its per-check state",
        "description": "**Required scope:** `stats:read` or `channels:read`\n\nA monitor of another tenant answers 404 like an unknown id.",
        "operationId": "getMonitor",
        "responses": {
          "200": {
            "description": "The monitor",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitor"
                },
                "example": {
                  "id": "0199b3e1-4f50-7a61-8b72-9c83d4e5f601",
                  "name": "ערוץ ראשי — QoE",
                  "target_kind": "channel",
                  "target_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                  "target_label": "חדשות 10",
                  "checks": [
                    {
                      "key": "feed_down",
                      "type": "feed_down",
                      "threshold": 0,
                      "for_s": 60,
                      "severity": "critical"
                    },
                    {
                      "key": "startup_p95",
                      "type": "startup_p95",
                      "threshold": 5000,
                      "for_s": 600,
                      "window_s": 600,
                      "severity": "warning"
                    }
                  ],
                  "destinations": [
                    "0199b3e0-aa11-7b22-8c33-d44e55f66a77"
                  ],
                  "inapp": true,
                  "notify_resolve": true,
                  "repeat_min": 0,
                  "quiet_hours": {
                    "from": "23:00",
                    "to": "07:00",
                    "tz": "Asia/Jerusalem",
                    "allow_critical": true
                  },
                  "snoozed_until": null,
                  "enabled": true,
                  "created_by": "0199a3c4-2b30-7d40-9e5f-6a7b8c9d0e1f",
                  "created_at": "2026-10-05T12:00:00Z",
                  "updated_at": "2026-10-05T12:00:00Z",
                  "status": "ok",
                  "states": [
                    {
                      "check_key": "feed_down",
                      "state": "ok",
                      "since": "2026-10-05T12:01:00Z",
                      "value": 0,
                      "evaluated_at": "2026-10-06T08:14:30Z"
                    },
                    {
                      "check_key": "startup_p95",
                      "state": "ok",
                      "since": "2026-10-05T12:01:00Z",
                      "value": 1840,
                      "evaluated_at": "2026-10-06T08:14:30Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read or channels:read"
      },
      "patch": {
        "tags": [
          "monitoring"
        ],
        "summary": "Change a monitor (removed checks resolve their open alerts; disabling resolves all)",
        "description": "**Required scope:** `notifications:manage`\n\nPartial update — only the fields present change (`quiet_hours: null` clears them); the merged monitor is validated\nas on create. Removing a check resolves its open alert; `enabled: false` resolves all of them. Body limit 64 KiB.\nAudits `monitor.update`.",
        "operationId": "patchMonitor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonitorInput"
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      },
      "delete": {
        "tags": [
          "monitoring"
        ],
        "summary": "Delete a monitor (its alert history stays)",
        "description": "**Required scope:** `notifications:manage`\n\nDeletes the monitor; its past alerts stay in the history with `monitor_name`. Audits `monitor.delete`.",
        "operationId": "deleteMonitor",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/monitors/{id}/snooze": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "Snooze / maintenance window — evaluation continues and alerts are recorded, nothing is sent (up to 7 days)",
        "description": "**Required scope:** `notifications:manage`\n\nGive `until` (a time in the next 7 days) or `minutes` (1–10080); `until` wins when both are present. While snoozed\nthe monitor reads `muted`, alerts are recorded with `muted: snoozed` and nothing is sent. `reason` is cut to 200\ncharacters. Replaces an existing snooze. Audits `monitor.snooze`.",
        "operationId": "snoozeMonitor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "minutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10080
                  },
                  "until": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              },
              "example": {
                "minutes": 120,
                "reason": "maintenance: encoder swap"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Snoozed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "snoozed_until": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "snooze_reason": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "snoozed_until": "2026-10-06T10:30:00Z",
                  "snooze_reason": "maintenance: encoder swap"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      },
      "delete": {
        "tags": [
          "monitoring"
        ],
        "summary": "End the snooze",
        "description": "**Required scope:** `notifications:manage`\n\nEnds the snooze at once (also when none is active). Audits `monitor.unsnooze`.",
        "operationId": "unsnoozeMonitor",
        "responses": {
          "204": {
            "description": "Snooze ended"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/service-status": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "Service status (\"תקינות השירות\")",
        "description": "**Required scope:** `stats:read` or `channels:read`\n\nService status (\"תקינות השירות\") — green/yellow/red per layer (streaming, delivery, origin, QoE) for your channels and hostnames\n\nEach layer combines the shared platform level (degraded / down when ViewStream's own monitoring sees a problem in\nthat layer — no infrastructure details) with your own monitors' firing checks on that layer. `unknown` = not\nmeasured (e.g. no QoE checks yet). `overall` is the worst layer level. `ai` lists the AI services separately and\ndoes not count towards `overall` (the lip-sync row is absent when lip-sync is turned off).",
        "operationId": "getServiceStatus",
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "overall": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded",
                        "down",
                        "unknown"
                      ]
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "layers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "layer": {
                            "type": "string",
                            "enum": [
                              "streaming",
                              "delivery",
                              "origin",
                              "qoe"
                            ]
                          },
                          "level": {
                            "type": "string",
                            "enum": [
                              "ok",
                              "degraded",
                              "down",
                              "unknown"
                            ]
                          },
                          "shared_level": {
                            "type": "string",
                            "enum": [
                              "ok",
                              "degraded",
                              "down",
                              "unknown",
                              ""
                            ]
                          },
                          "checks": {
                            "type": "integer"
                          },
                          "firing": {
                            "type": "integer"
                          },
                          "pending": {
                            "type": "integer"
                          },
                          "since": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "ai": {
                      "type": "array",
                      "description": "The AI services (Behema GPU) — levels only. `degraded` = delayed: the service is unavailable and its jobs\nwait in the queue (nothing is lost); `down` = unavailable for a long time. Not part of `overall`.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "service": {
                            "type": "string",
                            "enum": [
                              "subtitles",
                              "translation",
                              "upscale",
                              "lipsync"
                            ]
                          },
                          "level": {
                            "type": "string",
                            "enum": [
                              "ok",
                              "degraded",
                              "down",
                              "unknown"
                            ]
                          },
                          "since": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "overall": "ok",
                  "at": "2026-10-06T08:15:00Z",
                  "layers": [
                    {
                      "layer": "streaming",
                      "level": "ok",
                      "shared_level": "ok",
                      "checks": 2,
                      "firing": 0,
                      "pending": 0,
                      "since": "2026-10-05T22:00:00Z"
                    },
                    {
                      "layer": "delivery",
                      "level": "ok",
                      "shared_level": "ok",
                      "checks": 0,
                      "firing": 0,
                      "pending": 0,
                      "since": "2026-10-05T22:00:00Z"
                    },
                    {
                      "layer": "origin",
                      "level": "ok",
                      "shared_level": "ok",
                      "checks": 0,
                      "firing": 0,
                      "pending": 0,
                      "since": "2026-10-05T22:00:00Z"
                    },
                    {
                      "layer": "qoe",
                      "level": "ok",
                      "shared_level": "",
                      "checks": 2,
                      "firing": 0,
                      "pending": 0
                    }
                  ],
                  "ai": [
                    {
                      "service": "subtitles",
                      "level": "ok",
                      "since": "2026-10-05T07:00:00Z"
                    },
                    {
                      "service": "translation",
                      "level": "ok",
                      "since": "2026-10-05T07:00:00Z"
                    },
                    {
                      "service": "upscale",
                      "level": "degraded",
                      "since": "2026-10-06T07:40:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read or channels:read"
      }
    },
    "/v1/alerts": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "Alert history, newest first, with the delivery log of each alert",
        "description": "**Required scope:** `stats:read` or `channels:read`\n\nAlerts of the tenant ordered by `started_at`, newest first, each with its `sends` (one row per e-mail/Telegram\nattempt: firing, resolved, repeat). Page backwards with `before` = the `started_at` of the last alert you got.",
        "operationId": "listAlerts",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "description": "firing = still open, resolved = closed",
            "schema": {
              "type": "string",
              "enum": [
                "firing",
                "resolved"
              ]
            }
          },
          {
            "name": "monitor_id",
            "in": "query",
            "description": "Only alerts of this monitor",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "before",
            "in": "query",
            "description": "Only alerts that started before this RFC 3339 time (paging)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Alert"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "0199b4f2-6a70-7b81-9c92-ad03be14cf25",
                      "monitor_id": "0199b3e1-4f50-7a61-8b72-9c83d4e5f601",
                      "monitor_name": "ערוץ ראשי — QoE",
                      "check_key": "feed_down",
                      "check_type": "feed_down",
                      "severity": "critical",
                      "target_kind": "channel",
                      "target_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                      "target_label": "חדשות 10",
                      "value": 75,
                      "threshold": 0,
                      "title_he": "ערוץ חדשות 10: הפיד נפל — הצופים רואים סלייט",
                      "title_en": "channel חדשות 10: feed down — viewers see the slate",
                      "body_he": "אף מקודד לא מדווח שהפיד של ערוץ חדשות 10 פעיל כבר 60 שניות לפחות.",
                      "body_en": "No encoder has reported the feed of channel חדשות 10 up for at least 60 s.",
                      "link": "https://studio.viewstream.co.il/t/tv10poc/live/0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                      "started_at": "2026-10-06T06:02:10Z",
                      "resolved_at": "2026-10-06T06:09:40Z",
                      "acked_at": null,
                      "sends": [
                        {
                          "id": "0199b4f2-7b80-7c91-8da2-be13cf24d036",
                          "destination_id": "0199b3e0-aa11-7b22-8c33-d44e55f66a77",
                          "kind": "email",
                          "event": "firing",
                          "status": "sent",
                          "created_at": "2026-10-06T06:02:11Z"
                        },
                        {
                          "id": "0199b4f9-0c10-7d21-9e32-f4a5b6c7d8e9",
                          "destination_id": "0199b3e0-aa11-7b22-8c33-d44e55f66a77",
                          "kind": "email",
                          "event": "resolved",
                          "status": "sent",
                          "created_at": "2026-10-06T06:09:41Z"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read or channels:read"
      }
    },
    "/v1/alerts/{id}": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "One alert",
        "description": "**Required scope:** `stats:read` or `channels:read`\n\nThe alert with its delivery log. An alert of another tenant answers 404.",
        "operationId": "getAlert",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Alert id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The alert",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                },
                "example": {
                  "id": "0199b4f2-6a70-7b81-9c92-ad03be14cf25",
                  "monitor_id": "0199b3e1-4f50-7a61-8b72-9c83d4e5f601",
                  "monitor_name": "ערוץ ראשי — QoE",
                  "check_key": "feed_down",
                  "check_type": "feed_down",
                  "severity": "critical",
                  "target_kind": "channel",
                  "target_id": "0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                  "target_label": "חדשות 10",
                  "value": 75,
                  "threshold": 0,
                  "title_he": "ערוץ חדשות 10: הפיד נפל — הצופים רואים סלייט",
                  "title_en": "channel חדשות 10: feed down — viewers see the slate",
                  "body_he": "אף מקודד לא מדווח שהפיד של ערוץ חדשות 10 פעיל כבר 60 שניות לפחות.",
                  "body_en": "No encoder has reported the feed of channel חדשות 10 up for at least 60 s.",
                  "link": "https://studio.viewstream.co.il/t/tv10poc/live/0199a3c5-0c11-7a2b-8c3d-4e5f60718293",
                  "started_at": "2026-10-06T06:02:10Z",
                  "resolved_at": "2026-10-06T06:09:40Z",
                  "acked_at": null,
                  "sends": [
                    {
                      "id": "0199b4f2-7b80-7c91-8da2-be13cf24d036",
                      "destination_id": "0199b3e0-aa11-7b22-8c33-d44e55f66a77",
                      "kind": "email",
                      "event": "firing",
                      "status": "sent",
                      "created_at": "2026-10-06T06:02:11Z"
                    },
                    {
                      "id": "0199b4f9-0c10-7d21-9e32-f4a5b6c7d8e9",
                      "destination_id": "0199b3e0-aa11-7b22-8c33-d44e55f66a77",
                      "kind": "email",
                      "event": "resolved",
                      "status": "sent",
                      "created_at": "2026-10-06T06:09:41Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read or channels:read"
      }
    },
    "/v1/alerts/{id}/ack": {
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "Acknowledge an alert (stops repeats; idempotent — the first acknowledgement is kept)",
        "description": "**Required scope:** `notifications:manage` or `channels:operate`\n\nMarks the alert acknowledged by the caller with an optional `note` (cut to 500 characters); an acknowledged\nfiring alert is not re-sent by `repeat_min`. A second call changes nothing and returns the alert. The body is\noptional. Needs `notifications:manage` or `channels:operate`. Audits `alert.ack`.",
        "operationId": "ackAlert",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Alert id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              },
              "example": {
                "note": "המקודד הוחלף, בטיפול"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The alert",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage or channels:operate"
      }
    },
    "/v1/alert-destinations": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "Alert destinations (e-mail, Telegram) and whether the ViewStream Telegram bot is available",
        "description": "**Required scope:** `stats:read` or `channels:read`\n\nAlert destinations (e-mail, Telegram) and whether the ViewStream Telegram bot is available; targets are masked without notifications:manage\n\nAll destinations of the tenant (at most 50). Without `notifications:manage` the targets are masked (`n…@example.co.il`,\n`…1234` for Telegram chats). `telegram.configured` says whether Telegram destinations can be linked and `telegram.bot`\nnames the bot.",
        "operationId": "listAlertDestinations",
        "responses": {
          "200": {
            "description": "Destinations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "telegram"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AlertDestination"
                      }
                    },
                    "telegram": {
                      "type": "object",
                      "properties": {
                        "configured": {
                          "type": "boolean"
                        },
                        "bot": {
                          "type": "string"
                        }
                      }
                    },
                    "can_manage": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "0199b3e0-aa11-7b22-8c33-d44e55f66a77",
                      "kind": "email",
                      "name": "משמרת NOC",
                      "target": "noc@example.co.il",
                      "lang": "he",
                      "status": "active",
                      "verified_at": "2026-10-06T08:31:00Z",
                      "created_at": "2026-10-06T08:20:00Z",
                      "updated_at": "2026-10-06T08:31:00Z"
                    }
                  ],
                  "telegram": {
                    "configured": true,
                    "bot": "ViewStreamAlertsBot"
                  },
                  "can_manage": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read or channels:read"
      },
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "Add an e-mail destination",
        "description": "**Required scope:** `notifications:manage`\n\nAdd an e-mail destination — active at once for an active member of the tenant, otherwise pending until the recipient confirms the link mailed to them (double opt-in, 72 h)\n\nOnly `kind: email`; Telegram chats are added with `POST /v1/alert-destinations/telegram-link`. The address of an\nactive team member is `active` at once; any other address is `pending` and gets a confirmation mail whose link is\nvalid for 72 h (the relay is never an open mailer). `name` defaults to the address (max 120 chars), `lang` to `he`.\nAn address can be added once per tenant (409); at most 50 destinations (422). Body limit 8 KiB. Audits\n`alert_destination.create`.",
        "operationId": "createAlertDestination",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "target"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "email"
                    ]
                  },
                  "target": {
                    "type": "string",
                    "format": "email"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "lang": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ],
                    "default": "he"
                  }
                }
              },
              "example": {
                "kind": "email",
                "target": "noc@example.co.il",
                "name": "משמרת NOC",
                "lang": "he"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertDestination"
                },
                "example": {
                  "id": "0199b3e0-aa11-7b22-8c33-d44e55f66a77",
                  "kind": "email",
                  "name": "משמרת NOC",
                  "target": "noc@example.co.il",
                  "lang": "he",
                  "status": "pending",
                  "verify_sent_at": "2026-10-06T08:20:00Z",
                  "created_at": "2026-10-06T08:20:00Z",
                  "updated_at": "2026-10-06T08:20:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled (feature_disabled), or mail is not configured on this node for a pending address (not_ready)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/alert-destinations/telegram-link": {
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "A one-time Telegram deep link (15 min) to the ViewStream bot",
        "description": "**Required scope:** `notifications:manage`\n\nA one-time Telegram deep link (15 min) to the ViewStream bot — pressing Start in a chat (or adding the bot to a group with group_url) links that chat to the tenant\n\nReturns `url` (private chat) and `group_url` (add the bot to a group) for the ViewStream tenant bot; the chat that\nuses the link within 15 minutes is stored as a Telegram destination of this tenant. `lang` (`he` default,\nor `en`) is the language of that destination. One link per caller every 5 seconds. Audits\n`alert_destination.telegram_link`.",
        "operationId": "createTelegramLink",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lang": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ],
                    "default": "he"
                  }
                }
              },
              "example": {
                "lang": "he"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "group_url": {
                      "type": "string"
                    },
                    "bot": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "url": "https://t.me/ViewStreamAlertsBot?start=EXAMPLE-ONE-TIME-TOKEN",
                  "group_url": "https://t.me/ViewStreamAlertsBot?startgroup=EXAMPLE-ONE-TIME-TOKEN",
                  "bot": "ViewStreamAlertsBot",
                  "expires_at": "2026-10-06T08:35:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The Telegram bot could not be reached (internal_error)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Monitoring, or the Telegram bot, is not configured on this deployment (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/alert-destinations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "patch": {
        "tags": [
          "monitoring"
        ],
        "summary": "Rename, change the language, or disable / re-enable a destination",
        "description": "**Required scope:** `notifications:manage`\n\n`name` (1–120 chars), `lang` and `status` (`disabled`, or `active` again — only for a confirmed destination; a\npending address becomes active when the recipient confirms). `kind` and `target` cannot change (422; create a new\ndestination). Body limit 8 KiB. Audits `alert_destination.update`.",
        "operationId": "patchAlertDestination",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "lang": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ]
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "disabled"
                    ]
                  }
                }
              },
              "example": {
                "status": "disabled"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertDestination"
                },
                "example": {
                  "id": "0199b3e0-aa11-7b22-8c33-d44e55f66a77",
                  "kind": "email",
                  "name": "משמרת NOC",
                  "target": "noc@example.co.il",
                  "lang": "he",
                  "status": "disabled",
                  "verified_at": "2026-10-06T08:31:00Z",
                  "disabled_reason": "disabled in Studio",
                  "created_at": "2026-10-06T08:20:00Z",
                  "updated_at": "2026-10-06T09:00:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      },
      "delete": {
        "tags": [
          "monitoring"
        ],
        "summary": "Delete a destination (it is removed from every monitor)",
        "description": "**Required scope:** `notifications:manage`\n\nDeletes the destination; monitors that listed it stop sending to it. Audits `alert_destination.delete`.",
        "operationId": "deleteAlertDestination",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring is not enabled on this node (feature_disabled)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/alert-destinations/{id}/test": {
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "Send one clearly labelled test message to an active destination (1 per minute)",
        "description": "**Required scope:** `notifications:manage`\n\nSends a test alert to an `active` destination (409 otherwise). A delivery failure is not an HTTP error: the answer\nis 200 with `status: failed` and the reason. One test per destination per minute (429, `Retry-After: 60`).\nAudits `alert_destination.test`.",
        "operationId": "testAlertDestination",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "sent",
                        "failed"
                      ]
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "status": "sent"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Monitoring or alert delivery is not configured on this node (feature_disabled / not_ready)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/alert-destinations/{id}/resend": {
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "Mail the confirmation link again to a pending address (1 per 10 minutes; the old link stops working)",
        "description": "**Required scope:** `notifications:manage`\n\nOnly for a `pending` e-mail destination (409 otherwise). Issues a new 72-hour link (the previous one stops\nworking) and mails it. One mail per address per 10 minutes (429, `Retry-After: 600`). Audits\n`alert_destination.resend`.",
        "operationId": "resendAlertDestination",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "sent"
                      ]
                    }
                  }
                },
                "example": {
                  "status": "sent"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The confirmation mail could not be sent (internal_error)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Monitoring or mail is not configured on this node (feature_disabled / not_ready)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "notifications:manage"
      }
    },
    "/v1/cdn-byo": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "The tenant's own CDNs registered as steering pathways (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nBring-your-own pathways: the tenant's existing CDN as a steering pathway — typically the legacy pathway of a\nside-by-side migration. Interhost manages nothing at that CDN: the tenant registers its hostname, the signed-URL\nscheme its CDN validates (the issuer signs session URLs in it) and, optionally, its purge API. Secrets are\nwrite-only (`token_secret_set`, `purge_credentials_set`). A pathway joins the steering document only when it is\n`enabled` and steering is enabled for the tenant (`PUT /v1/steering`). `eligible`/`reason` as for partners\n(`disabled`, `killed`, `needs_token_auth` when a policy requires tokens and no scheme is set, `drm`).",
        "operationId": "listOwnCDNs",
        "responses": {
          "200": {
            "description": "Own-CDN pathways and the supported schemes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OwnCDN"
                      }
                    },
                    "token_schemes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "purge_kinds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      },
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Register an own CDN as a steering pathway (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nAt most 5 per tenant; `pathway_id` must not be one of the tenant's partner pathways or `il`. `hostname` is the\nhostname viewers use on that CDN (not an Interhost name). `token_scheme`: `none`, `akamai_edgeauth`,\n`cdn77_md5path`, `bunny_sha256`, `bunny_hs256`, `nginx_secure_link` (formats in docs/distributions.md, \"Token\nadapters\"); `token_secret` is the key that CDN validates with (Akamai: hex). `purge_kind`: `none`, `http` (POST/PUT\nof `{paths, urls}` to `purge_config.url` with one secret header), `cdn77` (`purge_config.resource_id`, API token as\n`purge_credentials`) or `bunny` (`purge_config.zone_id`, account API key). New pathways start disabled.",
        "operationId": "createOwnCDN",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OwnCDNInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnCDN"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "5 pathways already, or the pathway id is taken (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-byo/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "One own-CDN pathway (scope `stats:read`)",
        "operationId": "getOwnCDN",
        "responses": {
          "200": {
            "description": "The pathway",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnCDN"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read",
        "description": "**Required scope:** `stats:read`"
      },
      "patch": {
        "tags": [
          "delivery"
        ],
        "summary": "Change an own-CDN pathway; enable it to put it in the steering document (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nAbsent fields are unchanged; `token_secret` / `purge_credentials` \"\" removes the stored value; `pathway_id` cannot change.",
        "operationId": "patchOwnCDN",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OwnCDNInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The pathway",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnCDN"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      },
      "delete": {
        "tags": [
          "delivery"
        ],
        "summary": "Remove an own-CDN pathway (scope `delivery:write`)",
        "operationId": "deleteOwnCDN",
        "responses": {
          "204": {
            "description": "Removed (the next steering document leaves it out)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write",
        "description": "**Required scope:** `delivery:write`"
      }
    },
    "/v1/cdn-byo/{id}/check": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Sign one URL in the pathway's scheme and fetch it through that CDN (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\n`{path}` → a GET with `Range: bytes=0-1023` of `https://<hostname><signed path>` (10 s; the hostname must resolve to\npublic addresses). Answers `{ok, status, ms, bytes, cache, url_signed}` or `{ok: false, error}`.",
        "operationId": "checkOwnCDN",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "path"
                ],
                "properties": {
                  "path": {
                    "type": "string",
                    "example": "/live/tv10/main/master.m3u8"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Probe result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-partners/{id}/extras": {
      "put": {
        "tags": [
          "delivery"
        ],
        "summary": "Purge forwarding and cost per TB of a partner CDN (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\n`purge_forward` (default off): every purge job of the tenant is also sent to the partner zone (CDN77 purge API /\nBunny purge by URL) in the same job; the job result lists `forwarded: {pathway: \"ok\" | error}`. `cost_per_tb` (null =\nunknown) and `cost_currency` price the partner's traffic in `GET /v1/steering/compare`.",
        "operationId": "putCDNPartnerExtras",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "purge_forward": {
                    "type": "boolean"
                  },
                  "cost_per_tb": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0
                  },
                  "cost_currency": {
                    "type": "string",
                    "example": "USD"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored values",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "purge_forward": {
                      "type": "boolean"
                    },
                    "cost_per_tb": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "cost_currency": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/cdn-partners/{id}/parity": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Partner offload parity checklist",
        "description": "**Required scope:** `stats:read`\n\nPartner offload parity checklist — token, purge, hourly logs, cost attribution, shield pull (scope `stats:read`)\n\nFrom what is stored (no provider call): `items[]` of `{item, status: ok|off|partial|missing, detail}` for `token`,\n`purge`, `hourly_logs`, `cost_attribution`, `shield_pull`.",
        "operationId": "getCDNPartnerParity",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The checklist",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/steering/compare": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Side-by-side migration view — every pathway over the same period (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nPer pathway (ViewStream `il`, partners, own CDNs): sessions and share (beacon sessions by pathway), smoothness score,\nstartup p50, rebuffer ratio, error ratio (fatal sessions), bytes (`edge` for ViewStream, `provider` for partners with\nusage, else `estimated` from the beacon) and, when a cost per TB is stored, the cost. Default range: the last 24 h;\nat most 31 days. `stats_error` is set when the statistics store could not answer (the rest is still returned).",
        "operationId": "getSteeringCompare",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The comparison",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SteeringUnavailable"
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/distributions/{id}/edge-options": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Token adapter, playlist URL rewriting and third-party selector mode of a distribution (scope `stats:read`)",
        "operationId": "getDistributionEdgeOptions",
        "responses": {
          "200": {
            "description": "The options",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DistributionEdgeOptions"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Migration 0085 has not run (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read",
        "description": "**Required scope:** `stats:read`"
      },
      "put": {
        "tags": [
          "delivery"
        ],
        "summary": "Set the migration helpers of a CDN-only distribution (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\nFor external-origin (CDN-only) distributions. `token_adapter`: the signed-URL format the tenant already issues\n(`akamai_edgeauth`, `cdn77_md5path`, `bunny_sha256`, `bunny_hs256`, `nginx_secure_link`; `none` = off) with its key\n(`token_adapter_secret`, write-only) — the edge then accepts those URLs (alongside the in-house JWT when `token_mode`\nis `jwt`; not combinable with `hmac`). `carry_token` copies an accepted query token (Akamai ACL token) onto the\nURIs of the playlists. `rewrite_hosts`: hostnames (besides the origin's) whose absolute playlist URLs are rewritten\nto this hostname — active when `cache_rules.rewrite_absolute_urls` is true. `selector_mode` / `selector_label`:\nViewStream runs inside the tenant's own CDN selector; the label is the pathway value the tenant's player beacons\nreport for ViewStream. Applied by the edges on the next rules render.",
        "operationId": "putDistributionEdgeOptions",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token_adapter": {
                    "type": "string",
                    "enum": [
                      "none",
                      "akamai_edgeauth",
                      "cdn77_md5path",
                      "bunny_sha256",
                      "bunny_hs256",
                      "nginx_secure_link"
                    ]
                  },
                  "token_adapter_secret": {
                    "type": "string",
                    "writeOnly": true
                  },
                  "token_adapter_param": {
                    "type": "string",
                    "description": "query parameter / cookie name (Akamai default hdnts)"
                  },
                  "rewrite_hosts": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "carry_token": {
                    "type": "boolean"
                  },
                  "selector_mode": {
                    "type": "boolean"
                  },
                  "selector_label": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The options",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DistributionEdgeOptions"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Migration 0085 has not run (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}/adapter-sign": {
      "post": {
        "tags": [
          "delivery"
        ],
        "summary": "Sign a URL on the distribution in its token adapter's format (scope `delivery:write`)",
        "description": "**Required scope:** `delivery:write`\n\n`{path, expires_in_s}` (default 3600, at most 7 days) → `{url, scheme, expires_at}`. 409 when no adapter is set.",
        "operationId": "signDistributionAdapterURL",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "path"
                ],
                "properties": {
                  "path": {
                    "type": "string"
                  },
                  "expires_in_s": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "scheme": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "No token adapter (`conflict`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Migration 0085 has not run (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write"
      }
    },
    "/v1/distributions/{id}/selector-report": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "What ViewStream receives per ASN inside the tenant's own CDN selector (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\nEdge side (always): per ASN — requests, bytes, share of this hostname's bytes, 4xx+5xx ratio, 5xx count, cache hit\nratio, mean edge response time. Viewer side (only when the tenant's player sends the Insight beacon with\n`pathway` = `label`, default the distribution's `selector_label`): sessions on ViewStream, their share of the\ntenant's sessions on that ASN, smoothness score, startup p50 and rebuffer ratio. Default range 24 h, at most 31 days.",
        "operationId": "getDistributionSelectorReport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "label",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Migration 0085 has not run (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      }
    },
    "/v1/delivery/features": {
      "get": {
        "tags": [
          "delivery"
        ],
        "summary": "Per-tenant delivery switches (scope `stats:read`)",
        "description": "**Required scope:** `stats:read`\n\n`progressive_mp4` — MP4 files packaged on request from the stored renditions at `/m/mp4/vod/<tenant>/<asset>/<rendition|best>.mp4` (off by default).",
        "operationId": "getDeliveryFeatures",
        "responses": {
          "200": {
            "description": "The switches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "progressive_mp4": {
                      "type": "boolean"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Migration 0085 has not run (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "stats:read"
      },
      "put": {
        "tags": [
          "delivery"
        ],
        "summary": "Change the per-tenant delivery switches (scope `delivery:write`)",
        "operationId": "putDeliveryFeatures",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "progressive_mp4": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The switches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "progressive_mp4": {
                      "type": "boolean"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJSON"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Migration 0085 has not run (`feature_disabled`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "delivery:write",
        "description": "**Required scope:** `delivery:write`"
      }
    },
    "/reports/sla/{token}": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "A shared SLA or events report, read-only (v3 15.2; the token in the path is the secret)",
        "description": "The link made by POST /v1/channels/{id}/sla/share. No login. Answers while the link is not expired or revoked\nand the tenant's SLA terms are enabled; otherwise an HTML \"link no longer valid\" page. Every use is counted.\n`format=html` (default) | `pdf` | `csv` | `json`; `lang=he|en` overrides the link's language.",
        "operationId": "getSharedSLA",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "html",
                "pdf",
                "csv",
                "json"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report (HTML, PDF, CSV or JSON), or the \"no longer valid\" page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "PDF rendering is not configured",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/public/pricing": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "The public price list for the pricing calculator (v3 15.4; no tenant data)",
        "description": "The prices of the calculator's metrics (base_monthly, p95_mbps, delivered_tb, live_channel_days,\ntranscode_minutes, storage_vod_gb, drm_licences_k, ssai_impressions_k) from the one price list an Interhost admin\nmarked public; unpriced metrics are left out (\"priced on request\" on the page). Amounts are decimal strings,\nbefore VAT. CORS for `PRICING_ORIGINS` (www.viewstream.co.il). 404 when `PUBLIC_PRICING_ENABLED` is off or no\nlist is public. Cached 60 s.",
        "operationId": "getPublicPricing",
        "responses": {
          "200": {
            "description": "The public prices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "currency",
                    "version",
                    "name",
                    "estimate_only",
                    "vat_note",
                    "items"
                  ],
                  "properties": {
                    "currency": {
                      "type": "string"
                    },
                    "version": {
                      "type": "string",
                      "format": "date-time",
                      "description": "the list's last change"
                    },
                    "name": {
                      "type": "string"
                    },
                    "estimate_only": {
                      "type": "boolean"
                    },
                    "vat_note": {
                      "type": "object",
                      "properties": {
                        "en": {
                          "type": "string"
                        },
                        "he": {
                          "type": "string"
                        }
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "metric",
                          "unit",
                          "unit_price",
                          "label"
                        ],
                        "properties": {
                          "metric": {
                            "type": "string"
                          },
                          "unit": {
                            "type": "string"
                          },
                          "unit_price": {
                            "type": "string",
                            "description": "decimal"
                          },
                          "label": {
                            "type": "object",
                            "properties": {
                              "en": {
                                "type": "string"
                              },
                              "he": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "currency": "ILS",
                  "version": "2026-10-08T09:00:00Z",
                  "name": "Public 2026",
                  "estimate_only": true,
                  "vat_note": {
                    "en": "An estimate, before VAT, …",
                    "he": "הערכה בלבד, לפני מע״מ, …"
                  },
                  "items": [
                    {
                      "metric": "p95_mbps",
                      "unit": "Mbps",
                      "unit_price": "9.5",
                      "label": {
                        "en": "Bandwidth (95th percentile, commit floor)",
                        "he": "רוחב פס (אחוזון 95, מינימום התחייבות)"
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Prices are not published",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/public/pricing/leads": {
      "post": {
        "tags": [
          "billing"
        ],
        "summary": "Send the calculator's estimate by e-mail and leave contact details — only with explicit consent (v3 15.4)",
        "description": "Requires `consent: true` and the exact `consent_text` shown next to the checkbox; without consent nothing is\nstored (422). The estimate is recomputed from the public list and stored with the lead as computed (a later\nprice list never changes it), mailed to the visitor and, when `SALES_EMAIL` is set, copied to sales. The IP is\nstored only as a salted hash; at most 3 leads per IP hash and 100 overall per hour (429). Inputs are numbers\nor numeric strings (null = 0). A filled `website` field (a honeypot) answers 201 and stores nothing.",
        "operationId": "postPricingLead",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "email",
                  "consent",
                  "consent_text",
                  "inputs"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "company": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 4000
                  },
                  "lang": {
                    "type": "string",
                    "enum": [
                      "en",
                      "he"
                    ]
                  },
                  "consent": {
                    "type": "boolean",
                    "const": true
                  },
                  "consent_text": {
                    "type": "string"
                  },
                  "website": {
                    "type": "string",
                    "description": "honeypot; leave empty"
                  },
                  "inputs": {
                    "type": "object",
                    "properties": {
                      "delivery_mode": {
                        "type": "string",
                        "enum": [
                          "p95",
                          "tb"
                        ]
                      },
                      "p95_gbps": {
                        "type": [
                          "number",
                          "string",
                          "null"
                        ]
                      },
                      "tb_month": {
                        "type": [
                          "number",
                          "string",
                          "null"
                        ]
                      },
                      "live_channels": {
                        "type": [
                          "number",
                          "string",
                          "null"
                        ]
                      },
                      "vod_hours": {
                        "type": [
                          "number",
                          "string",
                          "null"
                        ]
                      },
                      "storage_tb": {
                        "type": [
                          "number",
                          "string",
                          "null"
                        ]
                      },
                      "drm_licences": {
                        "type": [
                          "number",
                          "string",
                          "null"
                        ]
                      },
                      "ad_impressions": {
                        "type": [
                          "number",
                          "string",
                          "null"
                        ]
                      },
                      "current_monthly_cost": {
                        "type": [
                          "number",
                          "string",
                          "null"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored (and mailed when `emailed`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "estimate",
                    "emailed"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "estimate": {
                      "type": "object"
                    },
                    "emailed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Prices are not published",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "No consent or invalid fields",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/channels/{id}/sla/monthly": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "Monthly SLA report of a channel",
        "description": "**Required scope:** `channels:read`\n\nMonthly SLA report of a channel — availability against the contracted target, causes, exclusions, credit (v3 15.2)\n\nOnly for tenants whose SLA terms an Interhost admin enabled (else 403 `feature_disabled`). The month is counted\nin the tenant's time zone (default: last month). Down minutes are split into platform, source feed (slate or\nfeed_down alert) and announced maintenance; SLA % = (eligible − platform) ÷ eligible, eligible = measured −\nsource − maintenance. Minutes with external probe coverage (two Israeli networks reporting) use the Gate 3 rule.\n`credit_pct` is the highest credit of the tenant's schedule whose `below_pct` the SLA % is under; when the month\nhas a non-void statement, `statement.credit_amount` = subtotal × credit %. `format=json` (default) | `csv` |\n`pdf` | `html`; `lang=he` (default) | `en`. Needs `channels:read`.",
        "operationId": "getChannelSLAMonthly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "month",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-(0[1-9]|1[0-2])$"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv",
                "pdf",
                "html"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SLAMonthly"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope, or SLA reports are not part of the tenant's agreement",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid or future month",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "PDF rendering is not configured",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/sla/events": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "Events report of a channel",
        "description": "**Required scope:** `channels:read`\n\nEvents report of a channel — feed losses, slates, failovers, encoder restarts and alerts with durations (v3 15.2)\n\nThe channel's alerts and feed events of the period with their durations (alerts until resolved, slate until\nslate_off) and a per-kind summary. Default: the last 7 days; at most 31. Same gate, formats and scope as the\nmonthly report.",
        "operationId": "getChannelSLAEvents",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv",
                "pdf",
                "html"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "he",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The events report",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope, or SLA reports are not part of the tenant's agreement",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid period",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read"
      }
    },
    "/v1/channels/{id}/sla/share": {
      "post": {
        "tags": [
          "monitoring"
        ],
        "summary": "Create a read-only, expiring share link of an SLA or events report (v3 15.2)",
        "description": "**Required scope:** `channels:write`\n\nAnswers the link once (`url`; only a hash of its token is stored). `kind=sla` needs `month`; `kind=events` needs\n`from` and `to` (at most 31 days). Expires after `expires_in_hours` (1–720, default 168); revoke with DELETE\n/v1/sla/shares/{id}. Audited as `sla_share.create`. Needs `channels:write`.",
        "operationId": "postSLAShare",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "sla",
                      "events"
                    ]
                  },
                  "month": {
                    "type": "string"
                  },
                  "from": {
                    "type": "string"
                  },
                  "to": {
                    "type": "string"
                  },
                  "lang": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ]
                  },
                  "expires_in_hours": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 720
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SLAShare"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope, or SLA reports are not part of the tenant's agreement",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such channel in this tenant",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write"
      }
    },
    "/v1/sla/shares": {
      "get": {
        "tags": [
          "monitoring"
        ],
        "summary": "The tenant's SLA / events share links (newest 200; tokens are never shown again)",
        "operationId": "listSLAShares",
        "responses": {
          "200": {
            "description": "Links",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SLAShare"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:read",
        "description": "**Required scope:** `channels:read`"
      }
    },
    "/v1/sla/shares/{id}": {
      "delete": {
        "tags": [
          "monitoring"
        ],
        "summary": "Revoke a share link (audited as `sla_share.revoke`)",
        "operationId": "revokeSLAShare",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such link",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "channels:write",
        "description": "**Required scope:** `channels:write`"
      }
    },
    "/v1/plan": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "The tenant's plan — tier, monthly limits, this month's usage, warnings, Starter price, first-run checklist",
        "description": "**Required scope:** none — any valid API key of the tenant.\n\nEvery member (the Studio banner). Custom tenants (every operator-created tenant): `tier: custom`, no limits.\nFree / Starter: `limits` (the plan's numbers), `usage` this calendar month in Asia/Jerusalem (storage is\nstanding), `percent` per cap and `warnings` (≥ 80 %), `resets_at` (the 1st), `player_badge`, the Starter\n`price` (before VAT, VAT %, total; `on_request` when no price is set), `payments_enabled` and the `checklist`\n(upload, embed, wordpress, invite, upgrade).",
        "operationId": "getPlan",
        "responses": {
          "200": {
            "description": "The plan",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/billing/upgrade": {
      "post": {
        "tags": [
          "billing"
        ],
        "summary": "Upgrade to Starter — a pending payment and the iCredit hosted payment page (the card is saved for renewals)",
        "description": "**Required scope:** `tenant:settings`\n\nCreates a payment (ILS, before VAT + the current VAT) and asks iCredit's GetUrl for a hosted page with\nCreateToken; the Rivhit receipt is issued by iCredit (same terminal group as Hostereo). The page returns to\nStudio → Settings → Billing; the payment counts only after the verified IPN. 409 `payments_unavailable` when\nno Starter price is set or ICREDIT_GROUP_TOKEN is empty (Studio shows \"contact us\"); 409 when already paid for\nmore than 5 days ahead or the tenant has a custom agreement. Allowed for a suspended tenant (to pay).",
        "operationId": "postBillingUpgrade",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lang": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ]
                  },
                  "phone": {
                    "type": "string"
                  },
                  "company": {
                    "type": "string"
                  },
                  "vat_id": {
                    "type": "string",
                    "description": "ח.פ / ת.ז for the receipt"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The payment page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payment_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "`payments_unavailable` (contact us) or `conflict`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "iCredit did not open a page",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope": "tenant:settings"
      }
    },
    "/v1/billing/payments": {
      "get": {
        "tags": [
          "billing"
        ],
        "summary": "The tenant's card payments with their Rivhit receipts",
        "operationId": "listBillingPayments",
        "responses": {
          "200": {
            "description": "Payments",
            "newest first": null,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SelfServePayment"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-required-scope": "billing:read",
        "description": "**Required scope:** `billing:read`"
      }
    },
    "/payments/icredit/ipn": {
      "post": {
        "tags": [
          "billing"
        ],
        "summary": "iCredit IPN (server to server) — believed only after iCredit's Verify answers VERIFIED; idempotent",
        "description": "Form or JSON from iCredit. `Custom1` names our payment; the amount must match it (±0.05) and iCredit's\nPaymentPageRequest.svc/Verify must answer VERIFIED for SaleId + amount. Then once: payment paid (sale id unique),\ntenant on Starter until the period end, card token sealed for renewals, receipt number/link kept, payment mail.\nA repeated IPN answers 200 and changes nothing. 404 without payment configuration.",
        "operationId": "postICreditIPN",
        "requestBody": {
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted"
          },
          "400": {
            "description": "Not an iCredit IPN",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Unknown payment or payments not configured",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Amount mismatch or not verified by iCredit",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": []
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "vs_<8-char prefix>_<32 random characters>",
        "description": "A tenant API key from Studio → Integrations → API keys, sent as `Authorization: Bearer <key>`.\nIn this page the key stays in the tab's memory only (never stored); reloading the page forgets it."
      }
    },
    "parameters": {
      "StatsFrom": {
        "name": "from",
        "in": "query",
        "description": "RFC 3339 or epoch ms; default to − 24 h",
        "schema": {
          "type": "string"
        }
      },
      "StatsTo": {
        "name": "to",
        "in": "query",
        "description": "RFC 3339 or epoch ms; default now",
        "schema": {
          "type": "string"
        }
      },
      "StatsCountry": {
        "name": "country",
        "in": "query",
        "description": "ISO 3166 alpha-2",
        "schema": {
          "type": "string"
        }
      },
      "StatsProgramme": {
        "name": "programme",
        "in": "query",
        "description": "EPG programme id (insight-v2 1.1): sessions attributed to it — breakdown, completion, qoe and sessions only",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "StatsDevice": {
        "name": "device",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "phone",
            "tablet",
            "desktop",
            "tv",
            "bot",
            "other"
          ]
        }
      },
      "StatsPathway": {
        "name": "pathway",
        "in": "query",
        "schema": {
          "type": "string"
        }
      },
      "StatsLive": {
        "name": "live",
        "in": "query",
        "schema": {
          "type": "boolean"
        }
      },
      "StatsChannel": {
        "name": "channel",
        "in": "query",
        "description": "channel slug or id",
        "schema": {
          "type": "string"
        }
      },
      "StatsAsset": {
        "name": "asset",
        "in": "query",
        "schema": {
          "type": "string"
        }
      },
      "StatsClip": {
        "name": "clip",
        "in": "query",
        "schema": {
          "type": "string"
        }
      },
      "StatsPageHost": {
        "name": "page_host",
        "in": "query",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid credentials. An unknown key prefix and a wrong secret get the same detail (\"API key is unknown or invalid\"). Failed key checks are limited per key prefix (10) and per client IP (30) per 10 minutes; past that the API answers 429 `rate_limited` with Retry-After before checking the key.",
        "headers": {
          "WWW-Authenticate": {
            "description": "Always `Bearer realm=\"viewstream\"`.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/invalid_credentials",
              "title": "Missing or invalid API key",
              "status": 401,
              "detail": "API key is revoked",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Scope, role or tenant mode forbids the route (`insufficient_scope`, `forbidden`, `feature_disabled`)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/feature_disabled",
              "title": "This feature is not enabled for a CDN-only tenant",
              "status": 403,
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Validation": {
        "description": "Validation failed; `errors[]` lists fields",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/validation_error",
              "title": "Validation failed",
              "status": 422,
              "detail": "invalid customer",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d",
              "errors": [
                {
                  "field": "slug",
                  "detail": "required"
                }
              ]
            }
          }
        }
      },
      "Conflict": {
        "description": "The change conflicts with the current state (`conflict`; e.g. last owner, not pending any more)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/conflict",
              "title": "Conflict",
              "status": 409,
              "detail": "a tenant must keep at least one active owner",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "BadJSON": {
        "description": "The body is not valid JSON, has unknown fields or is over the route's size cap (`validation_error`).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/validation_error",
              "title": "Validation failed",
              "status": 400,
              "detail": "invalid JSON body: unexpected EOF",
              "request_id": "01928f3a-7b10-7c4b-9a1b-3c5d7e9f1a04"
            }
          }
        }
      },
      "RateLimited": {
        "description": "20 requests/second per key exceeded; `Retry-After` in seconds",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "DeliveryInvalidJSON": {
        "description": "The body is not valid JSON, is too large, or has unknown fields (`validation_error`, status 400)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/validation_error",
              "title": "Validation failed",
              "status": 400,
              "detail": "invalid JSON body: json: unknown field \"asset\"",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "SteeringUnavailable": {
        "description": "Partner CDNs and steering are not enabled on this node (`feature_disabled`, status 503)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "IntegrationBadJSON": {
        "description": "The body is not valid JSON, is too large or has unknown fields (`validation_error`, status 400)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/validation_error",
              "title": "Validation failed",
              "status": 400,
              "detail": "invalid JSON body: json: unknown field \"secret\"",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "IntegrationInternalError": {
        "description": "Storage or secret-sealing failure (`internal_error`); safe to retry",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "EventExportUnavailable": {
        "description": "Event export is not available on this deployment (`feature_disabled`, status 503)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/feature_disabled",
              "title": "This feature is not enabled for a CDN-only tenant",
              "status": 503,
              "detail": "event export is not available",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "EventDestinationLimit": {
        "description": "The tenant already has the maximum number of destinations (5) (`conflict`)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/conflict",
              "title": "Conflict",
              "status": 409,
              "detail": "a tenant can have at most 5 event destinations",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "StatsUnavailable": {
        "description": "The statistics store failed or a query ran longer than 20 s (`not_ready`); retry later",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/not_ready",
              "title": "Not ready",
              "status": 503,
              "detail": "statistics store unavailable",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "InsightInternalError": {
        "description": "Unexpected server-side failure (`internal_error`); safe to retry",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ReportsUnavailable": {
        "description": "Scheduled reports are not configured on this control plane (`not_ready`)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ReportBadBody": {
        "description": "The body is not valid JSON, has unknown fields or is too large (`validation_error`)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/validation_error",
              "title": "Validation failed",
              "status": 400,
              "detail": "invalid JSON body",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "AuthInvalidJSON": {
        "description": "The body is not valid JSON, has unknown fields or exceeds 4 KiB (`validation_error`, status 400)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/validation_error",
              "title": "Validation failed",
              "status": 400,
              "detail": "invalid JSON body",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "SitesDisabled": {
        "description": "ViewStream Sites is not enabled on this deployment (`feature_disabled`)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/feature_disabled",
              "title": "This feature is not enabled for a CDN-only tenant",
              "status": 503,
              "detail": "ViewStream Sites is not enabled on this deployment",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      },
      "SitesBadRequest": {
        "description": "The body is not valid JSON, has unknown fields or is too large (`validation_error`)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://viewstream.co.il/problems/validation_error",
              "title": "Validation failed",
              "status": 400,
              "detail": "invalid JSON body: json: unknown field \"titel\"",
              "request_id": "019286a2-3b22-7c4b-9a1b-3c5d7e9f1a2d"
            }
          }
        }
      }
    },
    "schemas": {
      "SelfServePayment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenant_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "upgrade",
              "renewal"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "failed",
              "cancelled"
            ]
          },
          "amount": {
            "type": "string",
            "description": "before VAT"
          },
          "vat_pct": {
            "type": "string"
          },
          "total": {
            "type": "string",
            "description": "charged",
            "VAT included": null
          },
          "currency": {
            "type": "string"
          },
          "period_from": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "period_to": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "doc_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rivhit receipt number"
          },
          "doc_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "attempt": {
            "type": "integer"
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "SummarySettings": {
        "type": "object",
        "required": [
          "enabled",
          "available",
          "max_words"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "the tenant's switch (no row = off)"
          },
          "available": {
            "type": "boolean",
            "description": "false while the platform has summaries off (SUMMARIES_ENABLED=false); the tenant switch then cannot be turned on"
          },
          "max_words": {
            "type": "integer",
            "description": "the longest summary in words (one paragraph)",
            "example": 120
          }
        }
      },
      "AIArtifact": {
        "type": "object",
        "description": "An AI artefact: `body` is the kind's payload (an article: ArticleBody). Only\n`published` artefacts reach the site, the player and webhooks. `label` is the AI label to show.",
        "required": [
          "id",
          "kind",
          "subject_type",
          "subject_id",
          "status",
          "lang",
          "title",
          "body",
          "provenance",
          "auto",
          "label",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "article",
              "clip",
              "chips",
              "summary"
            ]
          },
          "subject_type": {
            "type": "string",
            "enum": [
              "programme",
              "asset"
            ]
          },
          "subject_id": {
            "type": "string",
            "format": "uuid"
          },
          "subject_title": {
            "type": "string"
          },
          "subject_start_at": {
            "type": "string",
            "format": "date-time",
            "description": "programmes: the guide start"
          },
          "channel": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "approved",
              "rejected",
              "published"
            ]
          },
          "lang": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "object",
            "additionalProperties": true
          },
          "provenance": {
            "type": "object",
            "additionalProperties": true,
            "description": "models / gateway routes per step, prompt version, source track id + version, tokens, corrections"
          },
          "auto": {
            "type": "boolean",
            "description": "published without review (auto-publish kinds)"
          },
          "label": {
            "type": "object",
            "properties": {
              "reviewed": {
                "type": "boolean"
              },
              "he": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            }
          },
          "reviewed_by": {
            "type": "string"
          },
          "reviewed_at": {
            "type": "string",
            "format": "date-time"
          },
          "reject_reason": {
            "type": "string"
          },
          "published_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AIClip": {
        "type": "object",
        "description": "A vertical-clip AI artefact (kind clip): in/out on the track's clock (epoch ms for recordings), the Hebrew title and text, and its render.",
        "required": [
          "id",
          "status",
          "title",
          "in_ms",
          "out_ms",
          "duration_ms",
          "score",
          "text",
          "provenance",
          "created_at",
          "subject_type",
          "subject_id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "approved",
              "rejected",
              "published"
            ],
            "description": "review status (the review queue owns it)"
          },
          "title": {
            "type": "string"
          },
          "in_ms": {
            "type": "integer",
            "format": "int64"
          },
          "out_ms": {
            "type": "integer",
            "format": "int64"
          },
          "duration_ms": {
            "type": "integer",
            "format": "int64"
          },
          "score": {
            "type": "number",
            "description": "0–10, how shareable"
          },
          "reason": {
            "type": "string"
          },
          "text": {
            "type": "string",
            "description": "the transcript of the clip"
          },
          "subject_type": {
            "type": "string",
            "enum": [
              "programme",
              "asset"
            ]
          },
          "subject_id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "provenance": {
            "type": "object",
            "additionalProperties": true,
            "description": "job_id, model, route (the gateway route that served the ranking), title_model, heuristic, track_id, track_version, tokens, rank"
          },
          "clip_id": {
            "type": "string",
            "format": "uuid",
            "description": "the /v1/clips clip its vertical render is the `vertical-9x16` variant of (set on the first render)"
          },
          "render": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "queued",
                  "running",
                  "ready",
                  "failed"
                ]
              },
              "job_id": {
                "type": "string",
                "format": "uuid"
              },
              "url": {
                "type": "string",
                "description": "the MP4 (1080×1920)"
              },
              "poster_url": {
                "type": "string"
              },
              "duration_ms": {
                "type": "integer",
                "format": "int64"
              },
              "bytes": {
                "type": "integer",
                "format": "int64"
              },
              "error": {
                "type": "string"
              },
              "rendered_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "ContentSummary": {
        "type": "object",
        "description": "The Hebrew content summary of one programme or library asset. `status` combines the\nstored summary with its newest `content_summary` job: `none` (nothing made yet), `pending` (job queued),\n`running`, `ready` (made by the model), `edited` (saved through PUT …/summary), `failed` (the job failed and no\nsummary exists; see `error`) or `waiting` (the summary model was unavailable; the job is retried by itself).",
        "required": [
          "subject_type",
          "subject_id",
          "enabled",
          "hebrew_ready",
          "status",
          "summary",
          "presenters",
          "presenters_source",
          "topics",
          "words",
          "max_words"
        ],
        "properties": {
          "subject_type": {
            "type": "string",
            "enum": [
              "programme",
              "asset"
            ]
          },
          "subject_id": {
            "type": "string",
            "format": "uuid"
          },
          "enabled": {
            "type": "boolean",
            "description": "summaries on for the tenant (and the platform)"
          },
          "hebrew_ready": {
            "type": "boolean",
            "description": "a ready Hebrew subtitle track with cues exists (the source)"
          },
          "status": {
            "type": "string",
            "enum": [
              "none",
              "pending",
              "running",
              "ready",
              "edited",
              "failed",
              "waiting"
            ]
          },
          "summary": {
            "type": "string",
            "description": "Hebrew, at most max_words words; empty until made"
          },
          "presenters": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "hosts / anchors of this episode: automatic names are verified per episode — a name of the show's presenters or of the guide is kept only when the episode's guide title or the opening of its Hebrew transcript (the first 10 minutes of speech from the programme's start) contains the full name (one-word names never)"
          },
          "presenters_source": {
            "type": "string",
            "enum": [
              "",
              "series",
              "epg",
              "transcript",
              "show",
              "editor"
            ],
            "description": "where the presenters came from: series = the show's presenters (Library → Series), epg = the guide — both verified for this episode; editor = saved through PUT …/summary; empty = none verified / unknown (never guessed since 2026-10-06; transcript/show are older values)"
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "words": {
            "type": "integer",
            "description": "words in summary"
          },
          "max_words": {
            "type": "integer",
            "example": 120
          },
          "source_version": {
            "type": "integer",
            "description": "the Hebrew track version it was made from (omitted for an edit made before any summary)"
          },
          "model": {
            "type": "string",
            "description": "the model that made it, e.g. `large+hebrew`"
          },
          "edited_by": {
            "type": "string",
            "description": "who saved the last edit: `key:<prefix>` for an API key, otherwise the user"
          },
          "edited_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "omitted while no summary is stored"
          },
          "error": {
            "type": "string",
            "description": "the failed job's error (status failed only)"
          }
        }
      },
      "PackagingRequest": {
        "type": "object",
        "description": "`mode` is trimmed and lower-cased; null (or an absent `mode`) clears the setting — the tenant falls back to fmp4, a channel or asset inherits the tenant.",
        "properties": {
          "mode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "fmp4",
              "ts",
              "both",
              null
            ]
          }
        }
      },
      "Packaging": {
        "type": "object",
        "description": "Effective output packaging with the rows it was resolved from",
        "required": [
          "mode",
          "source",
          "tenant",
          "urls",
          "warnings"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "fmp4",
              "ts",
              "both"
            ],
            "description": "The effective mode"
          },
          "source": {
            "type": "string",
            "enum": [
              "asset",
              "channel",
              "tenant",
              "default"
            ],
            "description": "Where `mode` came from (`default` = nothing set, fmp4)"
          },
          "tenant": {
            "$ref": "#/components/schemas/PackagingSetting"
          },
          "channel": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PackagingSetting"
              }
            ],
            "description": "Channel lookups only — the channel override row"
          },
          "asset": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PackagingSetting"
              }
            ],
            "description": "Asset lookups only — the asset override row"
          },
          "urls": {
            "type": "object",
            "description": "Playback URLs. Channel: `live`, `catch_up` (template with `{start_ms}`/`{end_ms}`); asset: `vod`. `fmp4` is null on the tenant route; `ts` only when the mode is ts or both.",
            "properties": {
              "fmp4": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": {
                  "type": "string"
                }
              },
              "ts": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "ts_disables_ll_hls",
                "ts_disables_multi_drm",
                "ts_larger_segments",
                "ts_alternate_no_ll_hls",
                "ts_alternate_no_drm"
              ]
            },
            "description": "Trade-offs of the effective mode (empty for fmp4)"
          }
        }
      },
      "PackagingSetting": {
        "type": "object",
        "description": "One stored packaging row; `mode` null = nothing stored at this level (then `updated_at` is absent)",
        "properties": {
          "mode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "fmp4",
              "ts",
              "both",
              null
            ]
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SubtitleCue": {
        "type": "object",
        "required": [
          "s",
          "e",
          "t"
        ],
        "properties": {
          "s": {
            "type": "integer",
            "format": "int64",
            "description": "start ms"
          },
          "e": {
            "type": "integer",
            "format": "int64",
            "description": "end ms"
          },
          "t": {
            "type": "string",
            "maxLength": 200,
            "description": "text; \\n = a line break (translations and edits keep their lines)"
          },
          "lc": {
            "type": "boolean",
            "description": "flagged for review (low ASR confidence, or a translation too long / too fast to read)"
          },
          "lang": {
            "type": "string",
            "enum": [
              "en"
            ],
            "description": "English speech inside a Hebrew track (tenant setting `foreign_speech`); absent for Hebrew"
          },
          "o": {
            "type": "string",
            "maxLength": 400,
            "description": "the original English when `t` is its Hebrew translation (`foreign_speech: translate`)"
          }
        }
      },
      "SubtitleTrack": {
        "type": "object",
        "description": "One subtitle track (one language of one programme, asset or clip)",
        "required": [
          "id",
          "subject_type",
          "subject_id",
          "lang",
          "name",
          "status",
          "version",
          "source",
          "word_level",
          "cues",
          "words",
          "low_conf_cues",
          "rtf",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "subject_type": {
            "type": "string",
            "enum": [
              "programme",
              "asset",
              "clip"
            ]
          },
          "subject_id": {
            "type": "string",
            "format": "uuid"
          },
          "lang": {
            "type": "string",
            "description": "he, en or ru"
          },
          "name": {
            "type": "string",
            "description": "the language's own name, as players list it (עברית, English, Русский)"
          },
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "failed",
              "disabled"
            ],
            "description": "disabled = automatic subtitles switched off for this video (not served)"
          },
          "version": {
            "type": "integer",
            "description": "bumped by every re-run or edit; PUT …/cues needs the current one"
          },
          "source": {
            "type": "string",
            "enum": [
              "asr",
              "mt",
              "edited",
              "uploaded"
            ],
            "description": "asr = recognised, mt = machine-translated from Hebrew, edited = saved in the editor"
          },
          "source_version": {
            "type": "integer",
            "description": "translations: the Hebrew version they were made from"
          },
          "model": {
            "type": "string",
            "description": "the model that made the track (e.g. `asr-he`, `large`)"
          },
          "stats": {
            "type": "object",
            "additionalProperties": true,
            "description": "translations: the job's counters (cues, requests, prompt/completion tokens, over_cps, over_length, latencies…)"
          },
          "start_at": {
            "type": "string",
            "format": "date-time",
            "description": "recordings: the window start (cue times are then epoch ms)"
          },
          "end_at": {
            "type": "string",
            "format": "date-time"
          },
          "word_level": {
            "type": "boolean",
            "description": "cue timings come from word timestamps"
          },
          "cues": {
            "type": [
              "integer",
              "null"
            ]
          },
          "words": {
            "type": [
              "integer",
              "null"
            ]
          },
          "low_conf_cues": {
            "type": [
              "integer",
              "null"
            ],
            "description": "cues flagged for review"
          },
          "rtf": {
            "type": [
              "number",
              "null"
            ],
            "description": "recognition real-time factor"
          },
          "vtt_url": {
            "type": "string",
            "description": "WebVTT on the tenant CDN hostname (omitted when none)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SubtitleSettings": {
        "type": "object",
        "description": "Subtitles settings of the tenant. Without a saved row: enabled, catch-up and VOD on,\nEnglish translation, the seeded financial-news glossary and glossary translations.",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "master switch"
          },
          "catchup": {
            "type": "boolean",
            "description": "subtitle finished programmes of the recordings"
          },
          "vod": {
            "type": "boolean",
            "description": "subtitle ready library assets"
          },
          "glossary": {
            "type": "string",
            "maxLength": 4000,
            "description": "names/terms, one per line; sent to the ASR model as the prompt"
          },
          "translate": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "en",
                "ru"
              ]
            },
            "description": "languages the Hebrew track is machine-translated into; stored de-duplicated in player order (en, ru)"
          },
          "glossary_translations": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 8000
            },
            "description": "per language (en, ru), one \"hebrew = translation\" pair per line (names and terms the translation must keep); seeded"
          },
          "foreign_speech": {
            "type": "string",
            "enum": [
              "translate",
              "keep"
            ],
            "default": "translate",
            "description": "English speech inside a Hebrew programme. `translate` (default): the Hebrew track shows it translated into\nHebrew (cues marked `lang: en` with the original English in `o`; WebVTT `<c.translated>`). `keep`: the\nHebrew track shows it in English (cues marked `lang: en`; WebVTT `<lang en>`). Either way the English\ntrack uses what was said in English instead of translating the Hebrew back. Detected per recognised\nsegment; stretches shorter than 2 s and single English words inside Hebrew sentences stay as transcribed.\nEditors (`assets:write`) may change it alone. Applies to subtitles made after the change."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "when the settings were last saved (zero time while never saved); ignored on writes"
          }
        }
      },
      "TenantBrandingInput": {
        "type": "object",
        "description": "change tenant-settings-sections — PUT replaces the whole branding: omitted or empty strings clear a field. Text fields lose control characters and < > \" and are trimmed.",
        "properties": {
          "display_name": {
            "type": "string",
            "maxLength": 80,
            "description": "shown instead of the tenant name (characters, after cleaning)"
          },
          "logo_light": {
            "type": "string",
            "maxLength": 1024,
            "description": "https URL of the logo for light backgrounds — on the tenant CDN hostname, *.vustream.net or *.viewstream.co.il"
          },
          "logo_dark": {
            "type": "string",
            "maxLength": 1024,
            "description": "https URL of the logo for dark backgrounds (same host rules)"
          },
          "primary_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "#rrggbb (stored lower-case); must reach 4.5:1 with its text colour"
          },
          "accent_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "#rrggbb (stored lower-case); must reach 4.5:1 with its text colour"
          },
          "favicon": {
            "type": "string",
            "maxLength": 1024,
            "description": "https URL of the favicon image (same host rules as the logos; the format is not checked)"
          },
          "email_from_name": {
            "type": "string",
            "maxLength": 60,
            "description": "sender display name of report and alert e-mails (the address stays ViewStream's)"
          }
        }
      },
      "TenantBranding": {
        "allOf": [
          {
            "$ref": "#/components/schemas/TenantBrandingInput"
          },
          {
            "type": "object",
            "required": [
              "display_name",
              "logo_light",
              "logo_dark",
              "primary_color",
              "accent_color",
              "favicon",
              "email_from_name",
              "effective",
              "contrast",
              "contrast_ok"
            ],
            "properties": {
              "updated_at": {
                "type": "string",
                "format": "date-time",
                "description": "absent when the tenant never saved a branding"
              },
              "updated_by": {
                "type": "string",
                "description": "actor of the last save (`key:<prefix>` or `user:<e-mail>`); absent when never saved"
              },
              "effective": {
                "type": "object",
                "description": "What the platform uses; colour fields are absent when no brand colour is set",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "display_name",
                    "else the tenant name": null
                  },
                  "primary_color": {
                    "type": "string"
                  },
                  "primary_ink": {
                    "type": "string",
                    "enum": [
                      "#ffffff",
                      "#0b1220"
                    ],
                    "description": "text colour on the primary colour (whichever contrasts more)"
                  },
                  "accent_color": {
                    "type": "string",
                    "description": "the accent colour",
                    "else the primary colour": null
                  },
                  "accent_ink": {
                    "type": "string",
                    "enum": [
                      "#ffffff",
                      "#0b1220"
                    ]
                  }
                }
              },
              "contrast": {
                "type": "array",
                "description": "Per set colour: `text / <colour>` (required, 4.5:1), `<colour> / light surface` (#ffffff) and `<colour> / dark surface` (#161b23) (advisory, 3:1). Empty when no colour is set.",
                "items": {
                  "type": "object",
                  "properties": {
                    "pair": {
                      "type": "string",
                      "example": "text / primary"
                    },
                    "ratio": {
                      "type": "number",
                      "description": "rounded to 2 decimals"
                    },
                    "min": {
                      "type": "number"
                    },
                    "ok": {
                      "type": "boolean"
                    },
                    "required": {
                      "type": "boolean",
                      "description": "a failing required pair blocks saving"
                    }
                  }
                }
              },
              "contrast_ok": {
                "type": "boolean",
                "description": "every required pair passes"
              }
            }
          }
        ]
      },
      "TenantDefaultsInput": {
        "type": "object",
        "description": "Only the keys present change; null resets a value to the platform default where allowed",
        "properties": {
          "retention_days": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 90,
            "description": "recording retention of NEW channels (null = platform 7)"
          },
          "dvr_window_s": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 60,
            "maximum": 86400,
            "description": "DVR window of NEW channels (null = platform 14400)"
          },
          "timezone": {
            "type": [
              "string",
              "null"
            ],
            "description": "IANA time zone (null = Asia/Jerusalem); `Local` and empty are rejected"
          },
          "locale": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "he",
              "en",
              null
            ],
            "description": "Studio default language (null = he)"
          },
          "auto_publish": {
            "type": "boolean",
            "description": "videos that become ready from now on publish automatically (false = stay unpublished); null is rejected"
          },
          "packaging": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "fmp4",
              "ts",
              "both",
              null
            ],
            "description": "tenant output packaging (trimmed, lower-cased; null = fmp4)"
          },
          "player_config_id": {
            "type": "string",
            "format": "uuid",
            "description": "make this player config the tenant default (needs delivery:write unless it already is); null is rejected"
          }
        }
      },
      "CatchupExclusionSettings": {
        "type": "object",
        "properties": {
          "grace_hours": {
            "type": "integer",
            "minimum": 0,
            "maximum": 168,
            "description": "Hours a programme excluded after it started keeps its recording (Undo window); default 24"
          },
          "updated_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "CatchupExclusionRuleInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "match": {
            "type": "string",
            "enum": [
              "exact",
              "prefix",
              "contains"
            ],
            "description": "exact = the whole normalised title; prefix / contains match on whole words (at least 3 letters). Default exact."
          },
          "pattern": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "EPG title (Hebrew-aware normalisation: whitespace, niqqud, final letters, quotes/geresh ״ ׳, dashes)"
          },
          "channel_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "null = every channel of the tenant"
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500
          },
          "enabled": {
            "type": "boolean",
            "description": "default true"
          }
        }
      },
      "CatchupExclusionRule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "channel_slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "match": {
            "type": "string",
            "enum": [
              "exact",
              "prefix",
              "contains"
            ]
          },
          "pattern": {
            "type": "string"
          },
          "normalized": {
            "type": "string",
            "description": "The pattern as it is compared"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "enabled": {
            "type": "boolean"
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "excluded": {
            "type": "integer",
            "description": "Programmes this rule currently excludes"
          }
        }
      },
      "CatchupExclusions": {
        "type": "object",
        "properties": {
          "settings": {
            "$ref": "#/components/schemas/CatchupExclusionSettings"
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CatchupExclusionRule"
            }
          },
          "suggestion": {
            "type": "string",
            "description": "Seed suggestion (the Shabbat placeholder title)"
          },
          "tail_margin_s": {
            "type": "integer",
            "description": "Seconds after its end before an excluded programme's recording may be deleted"
          }
        }
      },
      "CatchupExclusionPreview": {
        "type": "object",
        "properties": {
          "normalized": {
            "type": "string"
          },
          "upcoming": {
            "type": "integer"
          },
          "recent": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "programme_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "channel_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "channel_slug": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "start_at": {
                  "type": "string",
                  "format": "date-time"
                },
                "end_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "included": {
                  "type": "boolean",
                  "description": "A manual include keeps it in catch-up anyway"
                }
              }
            }
          }
        }
      },
      "CatchupExclusionStatus": {
        "type": "object",
        "description": "A programme's catch-up exclusion (absent on a programme list item = plainly included). Only `excluded` is always present; the other fields are omitted when they do not apply.",
        "required": [
          "excluded"
        ],
        "properties": {
          "excluded": {
            "type": "boolean"
          },
          "reason": {
            "type": "string",
            "enum": [
              "rule",
              "manual"
            ]
          },
          "rule_id": {
            "type": "string",
            "format": "uuid"
          },
          "rule_pattern": {
            "type": "string"
          },
          "rule_match": {
            "type": "string",
            "enum": [
              "exact",
              "prefix",
              "contains"
            ]
          },
          "override": {
            "type": "string",
            "enum": [
              "excluded",
              "included"
            ],
            "description": "The manual decision",
            "if any": null
          },
          "excluded_at": {
            "type": "string",
            "format": "date-time"
          },
          "delete_after": {
            "type": "string",
            "format": "date-time",
            "description": "When retention may delete its recording (Undo until then)"
          },
          "purged_at": {
            "type": "string",
            "format": "date-time",
            "description": "When its recording was deleted"
          }
        }
      },
      "ExcludedProgramme": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CatchupExclusionStatus"
          },
          {
            "type": "object",
            "properties": {
              "programme_id": {
                "type": "string",
                "format": "uuid"
              },
              "channel_id": {
                "type": "string",
                "format": "uuid"
              },
              "channel_slug": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "start_at": {
                "type": "string",
                "format": "date-time"
              },
              "end_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "state": {
                "type": "string",
                "enum": [
                  "excluded",
                  "purged",
                  "included"
                ]
              }
            }
          }
        ]
      },
      "TenantDefaults": {
        "type": "object",
        "required": [
          "player",
          "packaging",
          "subtitles",
          "recording",
          "timezone",
          "stored_timezone",
          "locale",
          "stored_locale",
          "protection",
          "auto_publish",
          "image_auto_upscale",
          "overrides",
          "ready"
        ],
        "properties": {
          "player": {
            "type": "object",
            "description": "The tenant's default player config and every config to choose from (`options` is null when the tenant has none)",
            "properties": {
              "config_id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "options": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "packaging": {
            "type": "object",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "fmp4",
                  "ts",
                  "both"
                ],
                "description": "effective tenant packaging"
              },
              "stored": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "the tenant's own setting (null = platform default fmp4)"
              }
            }
          },
          "subtitles": {
            "type": [
              "object",
              "null"
            ],
            "description": "null when subtitles are not available",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "catchup": {
                "type": "boolean"
              },
              "vod": {
                "type": "boolean"
              },
              "translate": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "recording": {
            "type": "object",
            "description": "Recording defaults of NEW channels; `stored_*` null = the platform value applies",
            "properties": {
              "retention_days": {
                "type": "integer"
              },
              "dvr_window_s": {
                "type": "integer"
              },
              "stored_retention_days": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "stored_dvr_window_s": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "platform_retention_days": {
                "type": "integer",
                "example": 7
              },
              "platform_dvr_window_s": {
                "type": "integer",
                "example": 14400
              }
            }
          },
          "timezone": {
            "type": "string",
            "description": "effective IANA time zone (platform default Asia/Jerusalem)"
          },
          "stored_timezone": {
            "type": [
              "string",
              "null"
            ]
          },
          "locale": {
            "type": "string",
            "enum": [
              "he",
              "en"
            ],
            "description": "effective Studio language (platform default he)"
          },
          "stored_locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "protection": {
            "type": "object",
            "description": "The tenant's default playback policy (policy_id null = none)",
            "properties": {
              "policy_id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "auto_publish": {
            "type": "boolean"
          },
          "image_auto_upscale": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "null when AI image upscaling is not available"
          },
          "overrides": {
            "type": "object",
            "description": "channels/assets that choose differently from each default (trashed ones excluded); `channels` and `assets` are the totals",
            "properties": {
              "player_channels": {
                "type": "integer"
              },
              "player_assets": {
                "type": "integer"
              },
              "player_sections": {
                "type": "integer"
              },
              "packaging_channels": {
                "type": "integer"
              },
              "packaging_assets": {
                "type": "integer"
              },
              "retention_channels": {
                "type": "integer",
                "description": "channels whose retention differs from the current default"
              },
              "dvr_channels": {
                "type": "integer"
              },
              "policy_channels": {
                "type": "integer"
              },
              "policy_assets": {
                "type": "integer"
              },
              "manual_publish_assets": {
                "type": "integer",
                "description": "assets uploaded as \"keep unpublished\""
              },
              "subtitle_optout_programmes": {
                "type": "integer"
              },
              "subtitle_optout_assets": {
                "type": "integer"
              },
              "subtitle_optout_clips": {
                "type": "integer"
              },
              "channels": {
                "type": "integer"
              },
              "assets": {
                "type": "integer"
              }
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "absent when the defaults were never saved"
          },
          "updated_by": {
            "type": "string",
            "description": "absent when the defaults were never saved"
          },
          "ready": {
            "type": "boolean",
            "description": "false before the tenant-settings migration (PUT answers 503)"
          }
        }
      },
      "BillingUsageValue": {
        "type": "object",
        "properties": {
          "value": {
            "type": [
              "number",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "no_data",
              "unavailable"
            ]
          },
          "as_of": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BillingUsage": {
        "type": "object",
        "properties": {
          "tenant": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "plan": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "description": "PoC when the tenant has no plan"
              },
              "source": {
                "type": "string"
              },
              "limits": {
                "type": "object"
              }
            }
          },
          "months": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "month": {
                  "type": "string"
                },
                "from": {
                  "type": "string",
                  "format": "date-time"
                },
                "to": {
                  "type": "string",
                  "format": "date-time"
                },
                "partial": {
                  "type": "boolean"
                },
                "metrics": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/BillingUsageValue"
                  }
                }
              }
            }
          },
          "daily": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "day": {
                  "type": "string"
                },
                "metrics": {
                  "type": "object",
                  "additionalProperties": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              }
            }
          },
          "sources": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "excluded": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/BillingUsageValue"
            }
          }
        }
      },
      "ImageSettings": {
        "type": "object",
        "description": "change ai-image-upscale — the tenant's AI image settings",
        "required": [
          "auto_upscale"
        ],
        "properties": {
          "auto_upscale": {
            "type": "boolean",
            "description": "Enlarge small posters, imported artwork and stills automatically (false when never set)"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true,
            "description": "Omitted when never set; ignored on PUT"
          },
          "updated_by": {
            "type": "string",
            "readOnly": true,
            "description": "`user:<e-mail>` or `key:<key prefix>`; omitted when never set; ignored on PUT"
          }
        }
      },
      "AIVideo": {
        "type": "object",
        "description": "change ai-video-upscale — one asset's AI 1080p rendition set",
        "required": [
          "asset_id",
          "status",
          "active",
          "limits"
        ],
        "properties": {
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "not_eligible",
              "none",
              "pending",
              "ready",
              "failed",
              "cancelled"
            ],
            "description": "none = eligible, never started; pending = job queued or running"
          },
          "reason": {
            "type": "string",
            "enum": [
              "large_enough",
              "too_small",
              "too_long",
              "no_video",
              "not_ready",
              "encrypted"
            ],
            "description": "Only with not_eligible"
          },
          "active": {
            "type": "boolean",
            "description": "the asset's master playlist plays the AI set"
          },
          "plan": {
            "type": "object",
            "description": "What the job does (absent for not_eligible without a probed source)",
            "properties": {
              "scale": {
                "type": "integer",
                "enum": [
                  2,
                  4
                ],
                "description": "model scale"
              },
              "model": {
                "type": "string",
                "example": "RealESRGAN_x2plus"
              },
              "src_w": {
                "type": "integer"
              },
              "src_h": {
                "type": "integer"
              },
              "out_w": {
                "type": "integer"
              },
              "out_h": {
                "type": "integer"
              },
              "fps": {
                "type": "number"
              },
              "frames": {
                "type": "integer"
              },
              "chunk_frames": {
                "type": "integer"
              },
              "chunks": {
                "type": "integer"
              },
              "duration_s": {
                "type": "number"
              }
            }
          },
          "estimate": {
            "type": "object",
            "description": "Shown before the job starts (calibrated GPU and wall time)",
            "properties": {
              "frames": {
                "type": "integer"
              },
              "gpu_s": {
                "type": "number"
              },
              "wall_s": {
                "type": "number"
              },
              "upscale_fps": {
                "type": "number"
              },
              "cost_ils": {
                "type": "number",
                "description": "when priced"
              }
            }
          },
          "progress": {
            "type": "object",
            "description": "Only while pending — the job's last report",
            "properties": {
              "percent": {
                "type": "number"
              },
              "message": {
                "type": "string"
              },
              "job_status": {
                "type": "string",
                "enum": [
                  "queued",
                  "dispatched",
                  "running",
                  "succeeded",
                  "failed",
                  "cancelled"
                ]
              },
              "attempts": {
                "type": "integer"
              },
              "started_at": {
                "type": "string",
                "format": "date-time"
              },
              "at": {
                "type": "string",
                "format": "date-time"
              },
              "remaining_s": {
                "type": "number",
                "description": "extrapolated after 3 % and 60 s"
              }
            }
          },
          "variant": {
            "type": "object",
            "description": "The stored AI version (absent when none was started)",
            "additionalProperties": true,
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "asset_id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "ready",
                  "failed",
                  "cancelled"
                ]
              },
              "active": {
                "type": "boolean"
              },
              "plan": {
                "type": "object"
              },
              "estimate": {
                "type": "object"
              },
              "out_prefix": {
                "type": "string",
                "description": "vod key prefix of the AI set (<tenant>/<asset>/ai/)"
              },
              "ai_master_key": {
                "type": "string"
              },
              "temporal_denoise": {
                "type": "boolean"
              },
              "result": {
                "type": "object",
                "additionalProperties": true,
                "description": "the job's result (model, scale, sizes, frames, chunks, chunks_reused, upscale_fps, gpu_s, …)"
              },
              "error": {
                "type": "string"
              },
              "job_id": {
                "type": "string",
                "format": "uuid"
              },
              "created_by": {
                "type": "string"
              },
              "switched_by": {
                "type": "string"
              },
              "switched_at": {
                "type": "string",
                "format": "date-time"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "preview_hls": {
            "type": "string",
            "format": "uri",
            "description": "Only when ready: the AI set's own master playlist (plays it before switching)"
          },
          "compare": {
            "type": "object",
            "description": "Only when ready: a short before/after pair",
            "properties": {
              "original": {
                "type": "string",
                "format": "uri"
              },
              "ai": {
                "type": "string",
                "format": "uri"
              },
              "at_s": {
                "type": "number"
              }
            }
          },
          "limits": {
            "type": "object",
            "properties": {
              "max_duration_s": {
                "type": "integer",
                "example": 3600
              },
              "target_short": {
                "type": "integer",
                "example": 1080
              }
            }
          }
        }
      },
      "SiteImage": {
        "type": "object",
        "description": "change sites-images — one image of a site's library",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "uploading",
              "processing",
              "ready",
              "failed"
            ],
            "description": "uploading = declared, waiting for the file; processing = image_ingest queued or running; ready = normalised file available; failed = see error (upload again to retry)"
          },
          "filename": {
            "type": "string",
            "description": "the declared file name",
            "cleaned": null
          },
          "content_type": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp",
              "image/avif"
            ],
            "description": "the declared type of the upload"
          },
          "size_bytes": {
            "type": "integer",
            "description": "the declared size of the upload"
          },
          "path": {
            "type": "string",
            "description": "origin path of the normalised image (use it in hero/promo image props, series images or the theme logo URL); absent until ready"
          },
          "width": {
            "type": "integer",
            "description": "pixels of the normalised file (absent until ready)"
          },
          "height": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer",
            "description": "size of the normalised file"
          },
          "focal": {
            "type": "object",
            "required": [
              "x",
              "y"
            ],
            "properties": {
              "x": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              },
              "y": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              }
            },
            "description": "focal point used by every crop (0,0 = top left; default 0.5, 0.5)"
          },
          "alt": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "alternative text per locale (he, en, ar, ru)"
          },
          "usage": {
            "type": "string",
            "enum": [
              "hero",
              "tile",
              "logo",
              "player_logo",
              "other"
            ]
          },
          "ingest": {
            "type": "object",
            "additionalProperties": true,
            "description": "image_ingest report: source/output metadata (exif, gps, xmp, iptc, icc, orientation, alpha), sizes, elapsed_ms"
          },
          "error": {
            "type": "string",
            "description": "why the upload or the ingest failed"
          },
          "job_id": {
            "type": "string",
            "format": "uuid",
            "description": "the image_ingest job"
          },
          "created_by": {
            "type": "string",
            "description": "who declared the image: user:<e-mail> or key:<key prefix>"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string",
            "description": "the normalised file on the tenant CDN (absent until ready)"
          },
          "preview": {
            "type": "string",
            "description": "the whole image ≤ 1280 px (imgproxy; absent without imgproxy)"
          },
          "crops": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "16x9 | 1x1 | 2x3 → imgproxy crop at the focal point (absent without imgproxy)"
          }
        }
      },
      "SiteImageUpload": {
        "type": "object",
        "description": "Where and how to send the file of a declared library image",
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "PUT"
            ]
          },
          "url": {
            "type": "string",
            "description": "same-origin upload path on this API: PUT /v1/images/{id}/content (browsers and API clients)"
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "headers to send with the PUT (Content-Type)"
          },
          "max_bytes": {
            "type": "integer",
            "description": "15728640 (15 MB)"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "30 minutes after the declaration (presigned_url stops working then)"
          },
          "presigned_url": {
            "type": "string",
            "description": "object-storage URL for in-network tools (absent when presigning is unavailable): PUT the file there, then POST /v1/images/{id}/complete"
          }
        }
      },
      "SiteImagePurge": {
        "type": "object",
        "description": "What a delete sent to the caches",
        "properties": {
          "edge_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "the original and its AI variant on the tenant CDN (an edge purge job); empty when the image never became ready"
          },
          "cloudflare_urls": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "every imgproxy URL of the image (each preset",
            "smart gravity and the focal point)": null
          },
          "cloudflare": {
            "type": "string",
            "enum": [
              "queued",
              "disabled"
            ],
            "description": "disabled = no Cloudflare purge configured on this deployment"
          }
        }
      },
      "SitePageSection": {
        "type": "object",
        "description": "One block instance of a layout document. `type` must be a registered block (hero, rail, rail_top10, grid, live_strip, epg, catchup_days, player, entity_header, people_rail, promo_tiles, podcast_list, text, ad_slot, html (needs sites:admin), countdown, search_box, search_results, continue_watching, accessibility_statement); `props` keys must be ones the block knows; `source` (kind manual, collection, show_episodes, tag, newest, most_watched, live_now, catchup, upcoming, people, related, search, person_items, current) must be one the block accepts.",
        "required": [
          "id",
          "type"
        ],
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64,
            "description": "unique within the layout"
          },
          "type": {
            "type": "string"
          },
          "props": {
            "type": "object",
            "additionalProperties": true
          },
          "source": {
            "type": "object",
            "additionalProperties": true,
            "description": "where the items come from: {kind, items? (manual, ≤ 100 of {type, id}), id?, series?, channel?, order?, limit? (0–100), window?, types?, tags?, days?, q?}; unknown fields are refused"
          },
          "visibility": {
            "type": "object",
            "additionalProperties": true,
            "description": "optional rules: from, to (RFC 3339 schedule window — outside it the Delivery API leaves the section out), devices, countries; other keys are refused"
          }
        }
      },
      "SitePageLayout": {
        "type": "object",
        "description": "The layout document of a page; at most 200 sections; unknown fields are refused",
        "required": [
          "v"
        ],
        "properties": {
          "v": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "sections": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/SitePageSection"
            }
          }
        }
      },
      "SitePage": {
        "type": "object",
        "description": "A page (kind page, served at path) or a template (kind template, renders every entity of template_for) with its draft",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "template"
            ]
          },
          "path": {
            "type": [
              "string",
              "null"
            ],
            "description": "page only: /, /shows, … (null for templates)"
          },
          "template_for": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "show",
              "asset",
              "clip",
              "programme",
              "channel",
              "person",
              "collection",
              "podcast",
              "search",
              "epg",
              "not_found",
              null
            ]
          },
          "title": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "per-locale title"
          },
          "draft": {
            "$ref": "#/components/schemas/SitePageLayout"
          },
          "draft_rev": {
            "type": "integer",
            "description": "revision of the draft; send it as If-Match when saving (also the ETag header)"
          },
          "published_version_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "the version last published immediately (scheduled versions do not change it)"
          },
          "locked_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "user holding the soft lock (expires 10 min after locked_at)"
          },
          "locked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "seo": {
            "type": "object",
            "additionalProperties": true,
            "description": "SEO overrides (title, description, noindex, …)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SitePageStatus": {
        "type": "object",
        "description": "Publishing state of a page now (the Studio pages tree)",
        "properties": {
          "live_version": {
            "type": [
              "integer",
              "null"
            ],
            "description": "the version on air now (null = not live)"
          },
          "latest_version": {
            "type": [
              "integer",
              "null"
            ],
            "description": "newest published version"
          },
          "draft_changed": {
            "type": "boolean",
            "description": "the draft differs from the newest published layout"
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "description": "next go_live_at in the future"
          },
          "takeover_ends": {
            "type": "string",
            "format": "date-time",
            "description": "go_off_at of the version on air"
          },
          "next_change": {
            "type": "string",
            "format": "date-time",
            "description": "earliest scheduled switch"
          }
        }
      },
      "SitePageVersion": {
        "type": "object",
        "description": "A published (or scheduled) snapshot of a page's draft",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "page_id": {
            "type": "string",
            "format": "uuid"
          },
          "version": {
            "type": "integer",
            "description": 1,
            "2": null,
            "… per page": null
          },
          "layout": {
            "$ref": "#/components/schemas/SitePageLayout"
          },
          "seo": {
            "type": "object",
            "additionalProperties": true
          },
          "title": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "published_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "the user (null for an API key)"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "go_live_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "scheduled start (null = live on publish)"
          },
          "go_off_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "takeover end — the previous version returns by itself"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SiteCheckResult": {
        "type": "object",
        "description": "site_check report: errors (accessibility — a11y_title, a11y_alt, invalid layout) block publishing; warnings (broken_link, empty, few_items, source_error, manual_missing, budget_sections > 25, budget_islands > 4, budget_live_hero, a11y_h1, contrast, …) do not",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "true when there are no errors"
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteCheckIssue"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteCheckIssue"
            }
          },
          "theme": {
            "type": "array",
            "description": "WCAG AA contrast rows of the theme (absent when empty)",
            "items": {
              "type": "object",
              "properties": {
                "pair": {
                  "type": "string"
                },
                "ratio": {
                  "type": "number"
                },
                "ok": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "SiteCheckIssue": {
        "type": "object",
        "properties": {
          "section": {
            "type": "string",
            "description": "section id (absent = the page or the site)"
          },
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "SitesDeliveryPresenter": {
        "type": "object",
        "description": "One presenter of a programme or an episode",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "description": "localised",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            ]
          },
          "slug": {
            "type": "string",
            "description": "the person's slug (a verified name that is one of the show's people)"
          },
          "image": {
            "type": "string",
            "description": "square portrait URL (tile_1x1)"
          },
          "url": {
            "type": "string",
            "description": "the person's page on the site (/person/{slug}) when the site has a person template"
          }
        }
      },
      "CompanyChip": {
        "type": "object",
        "description": "v3 entity-index: a company mentioned in a programme or video — jump the player to first_s",
        "required": [
          "slug",
          "name",
          "first_s",
          "count",
          "quote"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Hebrew name"
          },
          "name_en": {
            "type": "string"
          },
          "ticker": {
            "type": "string"
          },
          "exchange": {
            "type": "string",
            "enum": [
              "TASE",
              "NASDAQ",
              "NYSE"
            ]
          },
          "first_s": {
            "type": "number",
            "description": "first mention, seconds from the programme's play start (video: from 0)"
          },
          "count": {
            "type": "integer",
            "description": "lines that mention it"
          },
          "quote": {
            "type": "string",
            "description": "the first line that mentions it"
          }
        }
      },
      "TranscriptMoment": {
        "type": "object",
        "description": "v3 archive-search: where in a programme something was said",
        "required": [
          "offset_s",
          "text"
        ],
        "properties": {
          "offset_s": {
            "type": "number",
            "description": "seconds from the programme's play start (the catch-up player's position)"
          },
          "text": {
            "type": "string",
            "description": "the subtitle line"
          },
          "highlight": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "minItems": 2,
            "maxItems": 2,
            "description": "rune offset and length of the match in text (absent when it runs into the next line)"
          }
        }
      },
      "ImageUpscale": {
        "type": "object",
        "description": "change ai-image-upscale — one image's AI upscale state",
        "required": [
          "source_path",
          "original_url",
          "status",
          "active"
        ],
        "properties": {
          "source_path": {
            "type": "string",
            "description": "The origin path asked for (/vod/<tenant>/… or /rec/<tenant>/…)"
          },
          "original_url": {
            "type": "string",
            "format": "uri",
            "description": "The original on the tenant's CDN hostname"
          },
          "status": {
            "type": "string",
            "enum": [
              "none",
              "pending",
              "ready",
              "skipped",
              "failed",
              "reverted"
            ],
            "description": "none = never upscaled; pending = a job is queued or running; ready = the variant is served; skipped = not needed (see variant.error); failed = the job failed for good; reverted = an editor went back to the original (the variant is kept)"
          },
          "active": {
            "type": "boolean",
            "description": "imgproxy serves the variant (status ready; Studio shows the AI badge)"
          },
          "variant_url": {
            "type": "string",
            "format": "uri",
            "description": "The variant on the CDN; present once a variant was produced"
          },
          "variant": {
            "type": "object",
            "description": "The variant row; omitted when status is none",
            "required": [
              "id",
              "source_path",
              "variant_path",
              "status",
              "trigger",
              "created_at",
              "updated_at"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "source_path": {
                "type": "string"
              },
              "variant_path": {
                "type": "string",
                "description": "Next to the source: <name>_ai.jpg (PNG sources keep.png)"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "ready",
                  "skipped",
                  "failed",
                  "reverted"
                ]
              },
              "trigger": {
                "type": "string",
                "enum": [
                  "auto",
                  "manual"
                ]
              },
              "model": {
                "type": "string",
                "example": "RealESRGAN_x2plus"
              },
              "scale": {
                "type": "integer",
                "enum": [
                  2,
                  4
                ]
              },
              "src_w": {
                "type": "integer"
              },
              "src_h": {
                "type": "integer"
              },
              "out_w": {
                "type": "integer"
              },
              "out_h": {
                "type": "integer"
              },
              "timing": {
                "type": "object",
                "properties": {
                  "fetch_ms": {
                    "type": "integer"
                  },
                  "infer_ms": {
                    "type": "integer"
                  },
                  "remote_ms": {
                    "type": "integer"
                  },
                  "upload_ms": {
                    "type": "integer"
                  },
                  "total_ms": {
                    "type": "integer"
                  }
                }
              },
              "vram_peak_mb": {
                "type": "integer"
              },
              "error": {
                "type": "string",
                "description": "failed: the job error; skipped: large_enough | too_small | at_cap"
              },
              "job_id": {
                "type": "string",
                "format": "uuid",
                "description": "The image_upscale job of the latest request"
              },
              "created_by": {
                "type": "string",
                "description": "Who last requested or reverted it (`user:<e-mail>`, `key:<key prefix>`, or `auto` for the automatic sweep)"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "LipsyncSettings": {
        "type": "object",
        "description": "change lipsync-monitor; null = not set (tenant: off / default cadence, channel: inherit the tenant)",
        "properties": {
          "monitor": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "sample the recorded audio/video offset every interval_min minutes"
          },
          "correct": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "re-align the audio of recordings (catch-up, start-over, clips; never live) where drift is found"
          },
          "interval_min": {
            "type": [
              "integer",
              "null"
            ],
            "enum": [
              5,
              15,
              30,
              60,
              null
            ],
            "description": "monitor cadence (one GPU sample per interval); null = inherit, default 15"
          },
          "measure_at": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "recording",
              "input",
              "both",
              null
            ],
            "description": "v3 — where the monitor samples: recording (default; the catch-up recording), input (the source feed itself, HLS pull legs only) or both (each input sample paired with a recording sample at the same time; the difference is what ViewStream adds). null = inherit"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "when the row was last saved (omitted while none exists); ignored on writes"
          }
        }
      },
      "LipsyncTenantSettings": {
        "description": "The tenant's lip-sync defaults as GET/PUT /v1/lipsync/settings answer them",
        "allOf": [
          {
            "$ref": "#/components/schemas/LipsyncSettings"
          },
          {
            "type": "object",
            "required": [
              "available"
            ],
            "properties": {
              "available": {
                "type": "boolean",
                "description": "false when lip-sync is turned off on this platform (LIPSYNC_ENABLED=false) — Studio then hides every lip-sync control. Response only: sending it in a PUT is a 400 (unknown field)"
              }
            }
          }
        ]
      },
      "LipsyncEffective": {
        "type": "object",
        "properties": {
          "monitor": {
            "type": "boolean"
          },
          "correct": {
            "type": "boolean"
          },
          "interval_min": {
            "type": "integer"
          },
          "measure_at": {
            "type": "string",
            "enum": [
              "recording",
              "input",
              "both"
            ]
          },
          "tenant": {
            "$ref": "#/components/schemas/LipsyncSettings"
          },
          "channel": {
            "$ref": "#/components/schemas/LipsyncSettings"
          }
        }
      },
      "LipsyncComparison": {
        "type": "object",
        "description": "v3: input vs recording over the last hour (confident samples); added_ms = recording − input, i.e. what the path from our input to the recording adds",
        "properties": {
          "window_min": {
            "type": "integer"
          },
          "input": {
            "$ref": "#/components/schemas/LipsyncPointStatus"
          },
          "recording": {
            "$ref": "#/components/schemas/LipsyncPointStatus"
          },
          "added_ms": {
            "type": [
              "number",
              "null"
            ]
          },
          "legs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "leg": {
                  "type": "string"
                },
                "median_ms": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "confident_samples": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "LipsyncPointStatus": {
        "type": "object",
        "properties": {
          "point": {
            "type": "string",
            "enum": [
              "input",
              "recording"
            ]
          },
          "median_ms": {
            "type": [
              "number",
              "null"
            ],
            "description": "> 0 = audio early"
          },
          "confident_samples": {
            "type": "integer"
          },
          "samples": {
            "type": "integer"
          },
          "last_sample_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FastSource": {
        "type": "object",
        "description": "Library assets selected by tag (metadata.tags), library section or id (any of them)",
        "properties": {
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "collection_id": {
            "type": "string",
            "format": "uuid"
          },
          "asset_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "FastRules": {
        "type": "object",
        "properties": {
          "blocks": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "days": {
                  "type": "array",
                  "items": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 6
                  },
                  "description": "0 = Sunday; none = every day"
                },
                "from": {
                  "type": "string",
                  "example": "06:00"
                },
                "to": {
                  "type": "string",
                  "example": "12:00",
                  "description": "24:00 = midnight"
                },
                "source": {
                  "$ref": "#/components/schemas/FastSource"
                },
                "order": {
                  "type": "string",
                  "enum": [
                    "rotation",
                    "newest",
                    "random",
                    "sequential"
                  ]
                },
                "max_rating": {
                  "type": "string",
                  "enum": [
                    "all",
                    "8",
                    "12",
                    "14",
                    "16",
                    "18"
                  ]
                },
                "min_len_s": {
                  "type": "integer"
                },
                "max_len_s": {
                  "type": "integer"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/schemas/FastSource"
          },
          "repeat_hours": {
            "type": "integer",
            "minimum": 0,
            "maximum": 336,
            "description": "default 6"
          },
          "max_plays_per_day": {
            "type": "integer",
            "minimum": 0,
            "maximum": 48,
            "description": "default 3"
          },
          "fillers": {
            "$ref": "#/components/schemas/FastSource"
          },
          "break_every_min": {
            "type": "integer",
            "minimum": 0,
            "maximum": 240
          },
          "break_source": {
            "$ref": "#/components/schemas/FastSource"
          },
          "break_max_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 900
          }
        }
      },
      "FastBestOf": {
        "type": "object",
        "properties": {
          "approved": {
            "type": "boolean",
            "description": "an editor approved the rules (scope channels:operate); until then nothing airs automatically"
          },
          "hours": {
            "type": "integer",
            "description": "approved in the last N hours (default 24)"
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "min_len_s": {
            "type": "integer"
          },
          "max_len_s": {
            "type": "integer"
          },
          "order": {
            "type": "string",
            "enum": [
              "score",
              "newest"
            ]
          },
          "max_items": {
            "type": "integer"
          },
          "refresh_min": {
            "type": "integer"
          },
          "horizon_min": {
            "type": "integer"
          }
        }
      },
      "FastChannelInput": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
          },
          "title": {
            "type": "string",
            "maxLength": 200
          },
          "kind": {
            "type": "string",
            "enum": [
              "scheduled",
              "best_of"
            ]
          },
          "ladder": {
            "type": "string"
          },
          "ladder_policy": {
            "type": "string",
            "enum": [
              "exclude",
              "flag"
            ]
          },
          "enabled": {
            "type": "boolean"
          },
          "window_s": {
            "type": "integer",
            "minimum": 30,
            "maximum": 3600
          },
          "timezone": {
            "type": "string"
          },
          "rules": {
            "$ref": "#/components/schemas/FastRules"
          },
          "best_of": {
            "$ref": "#/components/schemas/FastBestOf"
          },
          "epg_enabled": {
            "type": "boolean"
          },
          "ssai_enabled": {
            "type": "boolean"
          },
          "ssai_channel_id": {
            "type": "string",
            "description": "a live channel id, or empty for none"
          },
          "live_channel_id": {
            "type": "string",
            "description": "the default break-in source, or empty for none"
          }
        }
      },
      "FastChannel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "scheduled",
              "best_of"
            ]
          },
          "ladder": {
            "type": "string"
          },
          "ladder_policy": {
            "type": "string",
            "enum": [
              "exclude",
              "flag"
            ]
          },
          "enabled": {
            "type": "boolean"
          },
          "window_s": {
            "type": "integer"
          },
          "timezone": {
            "type": "string"
          },
          "rules": {
            "$ref": "#/components/schemas/FastRules"
          },
          "best_of": {
            "$ref": "#/components/schemas/FastBestOf"
          },
          "epg_enabled": {
            "type": "boolean"
          },
          "ssai_enabled": {
            "type": "boolean"
          },
          "ssai_channel_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "live_channel_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "urls": {
            "type": "object",
            "properties": {
              "hls": {
                "type": "string",
                "description": "/m/fast/{tenant}/{slug}/master.m3u8 on the tenant's delivery hostname"
              },
              "hls_ssai": {
                "type": "string"
              },
              "epg_xmltv": {
                "type": "string"
              },
              "epg_json": {
                "type": "string"
              }
            }
          },
          "on_air": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FastItem"
              }
            ]
          },
          "next": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FastItem"
            }
          },
          "break_in": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FastBreakIn"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FastIssue": {
        "type": "object",
        "properties": {
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warning"
            ]
          },
          "code": {
            "type": "string",
            "enum": [
              "empty",
              "gap",
              "overlap",
              "off_grid",
              "missing_media",
              "not_ready",
              "encrypted",
              "ladder_mismatch",
              "ladder_profile",
              "rights",
              "rating",
              "truncated",
              "repeat",
              "break_length",
              "not_approved",
              "missing_rendition"
            ]
          },
          "message": {
            "type": "string"
          },
          "item_id": {
            "type": "string",
            "format": "uuid"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "suggest": {
            "type": "object",
            "properties": {
              "asset_id": {
                "type": "string",
                "format": "uuid"
              },
              "title": {
                "type": "string"
              },
              "duration_ms": {
                "type": "integer"
              }
            }
          }
        }
      },
      "FastCheck": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "errors": {
            "type": "integer"
          },
          "warnings": {
            "type": "integer"
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FastIssue"
            }
          },
          "deep": {
            "type": "boolean"
          },
          "took_ms": {
            "type": "integer"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FastItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "position": {
            "type": "integer"
          },
          "start_at": {
            "type": "string",
            "format": "date-time"
          },
          "end_at": {
            "type": "string",
            "format": "date-time"
          },
          "kind": {
            "type": "string",
            "enum": [
              "asset",
              "filler",
              "break",
              "clip"
            ]
          },
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "clip_id": {
            "type": "string",
            "format": "uuid"
          },
          "artifact_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "block": {
            "type": "string"
          },
          "in_ms": {
            "type": "integer"
          },
          "out_ms": {
            "type": "integer"
          },
          "active": {
            "type": "boolean",
            "description": "on air (its schedule is published and no later publish replaced it)"
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FastIssue"
            }
          }
        }
      },
      "FastEditItem": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "asset",
              "filler",
              "break",
              "clip"
            ]
          },
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "clip_id": {
            "type": "string",
            "format": "uuid"
          },
          "artifact_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "block": {
            "type": "string"
          },
          "in_ms": {
            "type": "integer"
          },
          "out_ms": {
            "type": "integer"
          },
          "duration_ms": {
            "type": "integer",
            "description": "slot length; default the media's duration (rounded up to whole segments)"
          }
        }
      },
      "FastSchedule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "checked",
              "blocked",
              "published",
              "superseded"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "rules",
              "manual",
              "best_of"
            ]
          },
          "check": {
            "$ref": "#/components/schemas/FastCheck"
          },
          "generated_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "published_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "item_count": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FastItem"
            }
          }
        }
      },
      "FastBreakIn": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "live_channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "manual",
              "scheduled"
            ]
          },
          "start_at": {
            "type": "string",
            "format": "date-time"
          },
          "end_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cancelled": {
            "type": "boolean"
          },
          "note": {
            "type": "string"
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "ended_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FastClipCandidate": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "format": "uuid"
          },
          "clip_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "score": {
            "type": "number"
          },
          "duration_ms": {
            "type": "integer"
          },
          "ladder": {
            "type": "string"
          },
          "approved_at": {
            "type": "string",
            "format": "date-time"
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "InputMonitorSettings": {
        "type": "object",
        "description": "v3 per-channel input monitoring (off by default)",
        "properties": {
          "enabled": {
            "type": "boolean",
            "default": false
          },
          "interval_s": {
            "type": "integer",
            "minimum": 5,
            "maximum": 60,
            "default": 10,
            "description": "snapshot cadence per leg"
          },
          "decode_every_s": {
            "type": "integer",
            "minimum": 10,
            "maximum": 300,
            "default": 30,
            "description": "sampled decode cadence (black / frozen / silence / loudness / format); ~5 s decoded each time"
          },
          "declared_kbps_a": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 50,
            "maximum": 200000
          },
          "declared_kbps_b": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 50,
            "maximum": 200000
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "ignored on writes"
          }
        }
      },
      "InputLegStatus": {
        "type": "object",
        "properties": {
          "snapshot": {
            "type": "object",
            "additionalProperties": true,
            "description": "the agent's newest report of the leg (see the example of GET /v1/channels/{id}/input-monitor)"
          },
          "conditions": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            }
          },
          "since": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "date-time"
            },
            "description": "since when each active condition holds"
          },
          "age_s": {
            "type": "number"
          }
        }
      },
      "InputCompare": {
        "type": "object",
        "description": "A/B comparison of the newest snapshots",
        "properties": {
          "both_up": {
            "type": "boolean"
          },
          "same_format": {
            "type": "boolean"
          },
          "format_a": {
            "type": "string"
          },
          "format_b": {
            "type": "string"
          },
          "kbps_ratio": {
            "type": [
              "number",
              "null"
            ],
            "description": "b / a"
          },
          "loudness_diff": {
            "type": [
              "number",
              "null"
            ],
            "description": "a − b (LU)"
          },
          "av_delta_diff_ms": {
            "type": [
              "number",
              "null"
            ]
          },
          "mismatch": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "format",
                "bitrate",
                "loudness",
                "av",
                "one_down"
              ]
            }
          }
        }
      },
      "LipsyncSpan": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "start_at": {
            "type": "string",
            "format": "date-time"
          },
          "end_at": {
            "type": "string",
            "format": "date-time"
          },
          "offset_ms": {
            "type": "number",
            "description": "measured (auto) or requested (manual) offset, > 0 = audio early"
          },
          "shift_frames": {
            "type": "integer"
          },
          "applied_ms": {
            "type": "number",
            "description": "shift_frames × 21.333 ms"
          },
          "source": {
            "type": "string",
            "enum": [
              "auto",
              "manual"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed",
              "disabled"
            ]
          },
          "confidence": {
            "type": "number"
          },
          "samples": {
            "type": "integer"
          },
          "segments": {
            "type": "integer"
          },
          "error": {
            "type": "string"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LipsyncStatus": {
        "type": "object",
        "description": "A channel's current lip-sync state from the confident samples of the last max(10 min, interval_min + 5 min)",
        "required": [
          "median_ms",
          "confident_samples",
          "state",
          "interval_min"
        ],
        "properties": {
          "median_ms": {
            "type": "number",
            "description": "median offset of the confident samples, 0.1 ms precision; > 0 = audio early"
          },
          "confident_samples": {
            "type": "integer"
          },
          "last_sample_at": {
            "type": "string",
            "format": "date-time",
            "description": "when the newest sample of the channel was taken (omitted when none)"
          },
          "state": {
            "type": "string",
            "enum": [
              "ok",
              "drift",
              "unknown"
            ],
            "description": "unknown = no confident sample; drift = |median_ms| > 120"
          },
          "interval_min": {
            "type": "integer",
            "description": "the effective monitor cadence"
          }
        }
      },
      "LipsyncSample": {
        "type": "object",
        "description": "One measurement of a 10 s window of the recording",
        "required": [
          "at",
          "window_ms",
          "kind",
          "status",
          "offset_ms",
          "confidence",
          "faces"
        ],
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "window_ms": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "monitor",
              "scan",
              "manual"
            ],
            "description": "monitor = the periodic sample; scan = dense samples around a drifting one; manual = measure input now"
          },
          "point": {
            "type": "string",
            "enum": [
              "recording",
              "input"
            ],
            "description": "v3: recording = the catch-up recording; input = the source feed"
          },
          "leg": {
            "type": "string",
            "enum": [
              "a",
              "b"
            ],
            "description": "input samples: the contribution leg"
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "no_face",
              "low_conf",
              "error"
            ]
          },
          "offset_ms": {
            "type": [
              "number",
              "null"
            ],
            "description": "> 0 = audio early"
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ],
            "description": "SyncNet confidence; below 3 the sample is not used"
          },
          "faces": {
            "type": "integer"
          },
          "spread_ms": {
            "type": "number",
            "description": "spread of the per-face offsets (omitted when unknown)"
          },
          "evidence_url": {
            "type": "string",
            "description": "short clip of the scored window on the tenant CDN (kept 7 days; omitted when none)"
          }
        }
      },
      "PlaybackPolicyRequest": {
        "type": "object",
        "description": "Create: `name` is required. Update: every field is optional; a present section replaces that section.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "Unique within the tenant"
          },
          "preset": {
            "type": "string",
            "enum": [
              "news"
            ],
            "description": "Build the rules from a preset first (sections in the same request override it)"
          },
          "domains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "with preset: the tenant's own domains (become referer_allow, embed_domains and cors_origins)"
          },
          "geo": {
            "$ref": "#/components/schemas/PlaybackPolicyGeo"
          },
          "hotlink": {
            "$ref": "#/components/schemas/PlaybackPolicyHotlink"
          },
          "drm": {
            "$ref": "#/components/schemas/PlaybackPolicyDRM"
          }
        }
      },
      "PlaybackTarget": {
        "type": "object",
        "description": "Name exactly one target (the first present of channel, asset, clip, catchup, startover is used).",
        "properties": {
          "channel": {
            "type": "string",
            "pattern": "^[A-Za-z0-9._-]{1,80}$",
            "description": "Channel slug (live)"
          },
          "asset": {
            "type": "string",
            "format": "uuid",
            "description": "Asset id (VOD)"
          },
          "clip": {
            "type": "string",
            "format": "uuid",
            "description": "Clip id (follows its channel's policy)"
          },
          "catchup": {
            "type": "object",
            "description": "A catch-up window of a channel",
            "properties": {
              "channel": {
                "type": "string",
                "description": "Channel slug"
              },
              "start": {
                "type": "string",
                "description": "Window start as in the catch-up URL ([A-Za-z0-9._-]{1,80})"
              },
              "end": {
                "type": "string",
                "description": "Window end",
                "same form": null
              }
            }
          },
          "startover": {
            "type": "object",
            "description": "Start-over of one programme",
            "properties": {
              "channel": {
                "type": "string",
                "description": "Channel slug"
              },
              "programme": {
                "type": "string",
                "description": "Programme id"
              }
            }
          }
        }
      },
      "PlaybackPolicyGeo": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "off",
              "allow",
              "deny"
            ],
            "default": "off"
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            },
            "description": "ISO 3166-1 alpha-2; at least one for allow/deny"
          },
          "asn_deny": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 4294967295
            }
          },
          "block_datacenter": {
            "type": "boolean",
            "description": "Refuse the datacenter/hosting ASN list"
          },
          "block_anonymous": {
            "type": "boolean",
            "description": "needs a paid database; rejected until bought"
          },
          "deny_action": {
            "type": "string",
            "enum": [
              "403",
              "slate"
            ],
            "default": "403",
            "description": "slate = the master playlist is replaced by the regional slate"
          }
        }
      },
      "PlaybackPolicyHotlink": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "enum": [
              "off",
              "required"
            ],
            "default": "off",
            "description": "required = every request needs a path token"
          },
          "ttl_live_s": {
            "type": "integer",
            "minimum": 60,
            "maximum": 604800,
            "default": 21600
          },
          "ttl_vod_extra_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 604800,
            "default": 7200,
            "description": "Added to the asset duration"
          },
          "bind": {
            "type": "string",
            "enum": [
              "none",
              "asn",
              "ip_prefix"
            ],
            "default": "none",
            "deprecated": true,
            "description": "Retired 2026-10-08: links are never bound to the viewer's address or network, so playback works behind VPNs, iCloud Private Relay and CGNAT carriers. Accepted for compatibility; asn and ip_prefix are stored and reported as none."
          },
          "referer_allow": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Host names; a `*.` prefix also matches the apex"
          },
          "allow_empty_referer": {
            "type": "boolean",
            "default": true,
            "description": "Apps and smart TVs send no Referer"
          },
          "cors_origins": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Origins like https://www.example.co.il; empty = *"
          },
          "embed_domains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "frame-ancestors of the hosted embed page"
          },
          "issue_rate_per_ip_min": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000,
            "default": 20
          },
          "max_streams": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "default": 0,
            "description": "Concurrent playback sessions per viewer (0 = off). Needs token required; applies to tokens issued with viewer_id (POST /v1/playback/tokens): the edge refuses a session beyond the limit with X-VS-Deny: concurrent_streams; the earliest sessions keep playing"
          },
          "stream_ttl_s": {
            "type": "integer",
            "minimum": 10,
            "maximum": 600,
            "description": "How long a session counts after its last playlist request (default 30 when max_streams is set)"
          }
        }
      },
      "PlaybackPolicyDRM": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "none",
              "aes128",
              "multi"
            ],
            "default": "none",
            "description": "aes128 = HLS AES-128 (needs hotlink.token required); multi is refused until a DRM vendor is configured"
          },
          "live": {
            "type": "boolean",
            "description": "aes128 only: also encrypt the live channels this policy covers"
          },
          "key_rotation": {
            "type": "string",
            "enum": [
              "period",
              "session",
              "daily"
            ],
            "description": "aes128; default period when live"
          },
          "key_rotation_minutes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1440,
            "description": "key_rotation period: live key period (default 10)"
          },
          "systems": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "multi-DRM (not available yet)"
          },
          "provider_id": {
            "type": "string",
            "description": "multi-DRM (not available yet)"
          },
          "scheme": {
            "type": "string",
            "description": "multi-DRM (not available yet)"
          },
          "hd_min_security": {
            "type": "string",
            "description": "multi-DRM (not available yet)"
          },
          "licence": {
            "type": "object",
            "description": "v3 licence policy (absent = none; the licence vendor's defaults). Applied to licences issued for keys\nof this policy (the in-house FairPlay key server; vendor adapters) and to the CPIX usage rules of per-track\nkeys. A device below a track's `min_security` gets no key for that track (e.g. a software-only browser plays\nUHD content at HD).",
            "properties": {
              "per_track_keys": {
                "type": "boolean",
                "description": "Separate keys for AUDIO, SD, HD, UHD1, UHD2 (CPIX ContentKeyUsageRules)"
              },
              "tracks": {
                "type": "object",
                "description": "Rules per track type (AUDIO, SD, HD, UHD1, UHD2; or ALL without per_track_keys)",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "min_security": {
                      "type": "string",
                      "enum": [
                        "sw_crypto",
                        "sw_decode",
                        "hw_crypto",
                        "hw_decode",
                        "hw_all"
                      ],
                      "description": "Widevine robustness 1–5; PlayReady sw_* → SL2000 (sw_crypto → SL150), hw_* → SL3000"
                    },
                    "hdcp": {
                      "type": "string",
                      "enum": [
                        "none",
                        "v1",
                        "v2",
                        "v2.1",
                        "v2.2",
                        "v2.3"
                      ]
                    }
                  }
                }
              },
              "cgms_a": {
                "type": "string",
                "enum": [
                  "copy_free",
                  "copy_once",
                  "copy_never"
                ]
              },
              "analog_output": {
                "type": "string",
                "enum": [
                  "allow",
                  "block"
                ]
              },
              "licence_duration_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 34560000,
                "description": "0 = unlimited"
              },
              "rental_duration_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 34560000
              },
              "playback_duration_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 34560000
              },
              "renewal_interval_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 34560000,
                "description": "0 = no renewal; else at least 30 s and shorter than licence_duration_s"
              },
              "persistent": {
                "type": "boolean",
                "default": false,
                "description": "Offline (persistent) licences; needs a licence or rental duration"
              }
            }
          }
        }
      },
      "PlaybackPolicyRules": {
        "type": "object",
        "description": "The normalised, enforceable rules (defaults filled in) — what the edge map carries.",
        "properties": {
          "geo": {
            "$ref": "#/components/schemas/PlaybackPolicyGeo"
          },
          "hotlink": {
            "$ref": "#/components/schemas/PlaybackPolicyHotlink"
          },
          "drm": {
            "$ref": "#/components/schemas/PlaybackPolicyDRM"
          }
        }
      },
      "PlaybackPolicy": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "is_default": {
            "type": "boolean",
            "description": "Attached to the tenant (the tenant default)"
          },
          "geo": {
            "$ref": "#/components/schemas/PlaybackPolicyGeo"
          },
          "hotlink": {
            "$ref": "#/components/schemas/PlaybackPolicyHotlink"
          },
          "drm": {
            "$ref": "#/components/schemas/PlaybackPolicyDRM"
          },
          "version": {
            "type": "integer",
            "description": "+1 on every change"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "rules": {
            "$ref": "#/components/schemas/PlaybackPolicyRules"
          },
          "protects": {
            "type": "boolean",
            "description": "false = the policy changes nothing at the edge (everything off)"
          },
          "attached": {
            "type": "object",
            "description": "Where the policy is attached (create answers zeros)",
            "properties": {
              "tenant": {
                "type": "boolean"
              },
              "channels": {
                "type": "integer"
              },
              "assets": {
                "type": "integer"
              },
              "distributions": {
                "type": "integer"
              }
            }
          }
        }
      },
      "PolicyAttachment": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "tenant",
              "channel",
              "asset",
              "distribution"
            ]
          },
          "target_id": {
            "type": "string",
            "format": "uuid"
          },
          "ref": {
            "type": "string",
            "description": "What the edge map uses: tenant slug, channel slug, asset id or hostname"
          },
          "label": {
            "type": "string",
            "description": "Display name of the target"
          },
          "policy_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "PlaybackSigningKey": {
        "type": "object",
        "description": "A playback signing key's metadata (the key itself is never returned here).",
        "properties": {
          "kid": {
            "type": "string",
            "example": "k2"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "retire_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "null = the current signer; else verifies tokens until this time"
          },
          "exported_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Downloaded for self-signing at"
          }
        }
      },
      "PolicyDecision": {
        "type": "object",
        "description": "What an edge would do with the request.",
        "properties": {
          "allow": {
            "type": "boolean"
          },
          "reason": {
            "type": "string",
            "enum": [
              "token_missing",
              "token_invalid",
              "token_expired",
              "token_bound",
              "revoked",
              "geo",
              "asn",
              "datacenter",
              "referer"
            ],
            "description": "The X-VS-Deny value when refused"
          },
          "slate": {
            "type": "boolean",
            "description": "The master playlist would be replaced by the regional slate"
          },
          "cors": {
            "type": "string",
            "description": "Access-Control-Allow-Origin the edge would send (empty = none)"
          },
          "sid": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "asn": {
            "type": "integer"
          },
          "datacenter": {
            "type": "boolean"
          },
          "token_exp": {
            "type": "integer",
            "description": "Token expiry (Unix seconds)"
          }
        }
      },
      "ProtectionBlackout": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "programme_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "start_at": {
            "type": "string",
            "format": "date-time"
          },
          "end_at": {
            "type": "string",
            "format": "date-time"
          },
          "geo": {
            "type": "object",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "allow",
                  "deny"
                ]
              },
              "countries": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "action": {
            "type": "string",
            "enum": [
              "slate",
              "403"
            ]
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The Studio user; null for API keys"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WatermarkAsset": {
        "type": "object",
        "description": "An asset's A/B watermark variants",
        "properties": {
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "none",
              "queued",
              "ready",
              "failed",
              "off"
            ]
          },
          "base_key": {
            "type": "string",
            "description": "The rendition prefix the variants belong to"
          },
          "rungs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "segment_ms": {
            "type": "integer"
          },
          "segments": {
            "type": "integer"
          },
          "strength": {
            "type": "integer"
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "error": {
            "type": "string"
          },
          "stale": {
            "type": "boolean",
            "description": "The asset was re-encoded after the variants were made; they are no longer mixed"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WatermarkDetection": {
        "type": "object",
        "description": "A trace of a leaked recording",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "asset_title": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "awaiting_sample",
              "queued",
              "scoring",
              "done",
              "failed"
            ]
          },
          "sample_url": {
            "type": "string"
          },
          "sample_bytes": {
            "type": "integer",
            "format": "int64"
          },
          "since": {
            "type": "string",
            "format": "date-time"
          },
          "until": {
            "type": "string",
            "format": "date-time"
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "result": {
            "type": "object",
            "description": "worker: {offset_ms, align_score, sample_ms, segments: [{n, z}]} (z > 0 = variant A); candidates: the best 20 sessions {sid, score, agree, p_value, confidence, requests, first_seen, last_seen, countries, asns}; scored: sessions scored; note"
          },
          "top_sid": {
            "type": "string"
          },
          "confidence": {
            "type": "number"
          },
          "leak_id": {
            "type": "string",
            "format": "uuid",
            "description": "The leak case it opened or joined"
          },
          "error": {
            "type": "string"
          },
          "created_by": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProtectionLeak": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "sid": {
            "type": "string",
            "description": "Playback session id"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "revoked",
              "dismissed"
            ]
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which thresholds were crossed"
          },
          "prefixes": {
            "type": "integer",
            "description": "Distinct networks (/24 or /48)"
          },
          "asns": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer",
            "format": "int64"
          },
          "requests": {
            "type": "integer",
            "format": "int64"
          },
          "ip_hashes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Hashed viewer IPs (never the addresses)"
          },
          "asn_list": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "user_agents": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Samples"
          },
          "paths": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "first_seen": {
            "type": "string",
            "format": "date-time"
          },
          "last_seen": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProtectionRevocation": {
        "type": "object",
        "properties": {
          "sid": {
            "type": "string"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who revoked: `user:<e-mail>`, `key:<key prefix>`, or the leak job"
          },
          "leak_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProtectionSettings": {
        "type": "object",
        "properties": {
          "leak_prefixes": {
            "type": "integer",
            "description": "N: distinct networks per sid in 10 min (default 3)"
          },
          "leak_asns": {
            "type": "integer",
            "description": "M: distinct ASNs per sid in 10 min (default 2)"
          },
          "leak_bytes_x": {
            "type": "number",
            "description": "X: multiple of one viewer's top-rung bytes (default 3)"
          },
          "top_rung_kbps": {
            "type": "integer",
            "description": "default 5000"
          },
          "auto_revoke": {
            "type": "boolean",
            "description": "default false"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Zero time when never saved"
          }
        }
      },
      "DistributionConfig": {
        "type": "object",
        "description": "v3: the editable, non-secret configuration of a distribution as one document (`GET`/`PUT\n/v1/distributions/{id}/config`, the stored versions). Fields as in `DistributionInput`; unknown fields (secrets\nincluded) are refused with 400. An omitted list or object is applied as empty; an omitted `origin_kind`,\n`token_mode` or `enabled` stays as it is.",
        "properties": {
          "hostname": {
            "type": "string",
            "description": "Must equal the distribution's hostname (immutable)"
          },
          "origin_kind": {
            "type": "string",
            "enum": [
              "library",
              "external"
            ]
          },
          "origin_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "origin_host_header": {
            "type": [
              "string",
              "null"
            ]
          },
          "token_mode": {
            "type": "string",
            "enum": [
              "none",
              "hmac",
              "jwt"
            ]
          },
          "cors_origins": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "geo_allow": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "geo_deny": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "cache_rules": {
            "type": "object",
            "description": "As `DistributionInput.cache_rules`"
          },
          "enabled": {
            "type": "boolean"
          },
          "origin_options": {
            "type": "object",
            "description": "As `DistributionInput.origin_options`"
          },
          "log_export": {
            "type": "object",
            "description": "As `DistributionInput.log_export` (without `enabled_at`)"
          }
        }
      },
      "DistributionInput": {
        "type": "object",
        "properties": {
          "hostname": {
            "type": "string",
            "description": "Create only (required there); lower-case DNS name, ≤ 253 characters; immutable"
          },
          "origin_kind": {
            "type": "string",
            "enum": [
              "library",
              "external"
            ],
            "description": "library = the ViewStream origin (platform tenants; their default); external = origin_url (CDN-only default)"
          },
          "origin_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "external only: http(s)://host[:port], no path, credentials, query or fragment; must not resolve to private/Interhost/edge addresses"
          },
          "origin_host_header": {
            "type": [
              "string",
              "null"
            ],
            "description": "external only — Host header sent to the origin"
          },
          "token_mode": {
            "type": "string",
            "enum": [
              "none",
              "hmac",
              "jwt"
            ],
            "default": "none"
          },
          "cors_origins": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "string"
            },
            "description": "`https://host[:port]` origins; empty = Access-Control-Allow-Origin: *"
          },
          "geo_allow": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            },
            "description": "ISO 3166-1 alpha-2 (normalised to upper case)"
          },
          "geo_deny": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            }
          },
          "cache_rules": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "query_keys": {
                "type": "array",
                "maxItems": 10,
                "items": {
                  "type": "string",
                  "pattern": "^[A-Za-z0-9_.-]{1,64}$"
                },
                "description": "Query parameters kept in the cache key"
              },
              "playlist_ttl_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 31536000,
                "description": "Live playlist TTL when the origin sends no Cache-Control (external)"
              },
              "vod_playlist_ttl_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 31536000
              },
              "segment_ttl_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 31536000
              },
              "rewrite_absolute_urls": {
                "type": "boolean",
                "description": "Recorded; not applied by the edge yet"
              },
              "preset": {
                "type": "string",
                "enum": [
                  "",
                  "streaming"
                ],
                "description": "external only: `streaming` appends the streaming defaults after `rules` — `.key` / `/keys/` never cached, `.m3u8` 1 s and `.mpd` 2 s with stale on origin error, segments (ts, m4s, aac, vtt, cmfv, cmfa) 1 day with stale"
              },
              "rules": {
                "type": "array",
                "maxItems": 20,
                "description": "external only: ordered path rules tried before the default locations (the first match wins). A rule whose pattern names m3u8 / mpd applies to playlists only; any other rule never matches a playlist. Never matches /b/ or /v1/, nor (hmac) tokenised paths. TTLs apply when the origin sends no Cache-Control / Expires.",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "match",
                    "pattern"
                  ],
                  "properties": {
                    "match": {
                      "type": "string",
                      "enum": [
                        "prefix",
                        "suffix",
                        "regex"
                      ]
                    },
                    "pattern": {
                      "type": "string",
                      "description": "prefix: `/…` (letters, digits, _. / -); suffix: e.g. `.m3u8`; regex: letters, digits and _. * + ?  | [ ] ^ $ \\ / - (RE2 syntax, matched by the edge as PCRE)"
                    },
                    "ttl_s": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 31536000,
                      "description": "Required unless bypass"
                    },
                    "bypass": {
                      "type": "boolean",
                      "description": "Never cached (no ttl_s / stale_s)"
                    },
                    "stale_s": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 31536000,
                      "description": "> 0 serves stale on origin error / timeout (non-playlists also while refreshing in the background); the edge cache's inactive time bounds it, not this value"
                    }
                  }
                }
              },
              "tag_capture": {
                "type": "boolean",
                "description": "external only: the edges record the origin's `Cache-Tag` / `Surrogate-Key` per cached object so `POST /v1/purge {\"tags\": […]}` can remove them"
              }
            }
          },
          "origin_options": {
            "type": "object",
            "additionalProperties": false,
            "description": "external only; `{}` = off (the edge config is unchanged). Failover is passive: `max_fails` errors / timeouts / 502-504 within `fail_timeout_s` take an origin out for `fail_timeout_s`; the backup serves only while the primary is out, and the primary is retried after `fail_timeout_s` (failback).",
            "properties": {
              "backup_url": {
                "type": "string",
                "description": "Backup origin, same scheme as origin_url, different host; must answer for the primary's Host header (the same Host and TLS SNI name are sent)"
              },
              "max_fails": {
                "type": "integer",
                "minimum": 0,
                "maximum": 10,
                "description": "Default 3 with a backup; 0 = never take the origin out"
              },
              "fail_timeout_s": {
                "type": "integer",
                "minimum": 5,
                "maximum": 600,
                "description": "Default 30 with a backup"
              },
              "max_conns": {
                "type": "integer",
                "minimum": 0,
                "maximum": 10000,
                "description": "Origin protection cap: concurrent connections per edge per origin server (0 = unlimited); over the cap the request goes to the backup or answers 502"
              },
              "auth_header": {
                "type": "string",
                "pattern": "^(X-[A-Za-z0-9-]{1,60}|Authorization)$",
                "description": "Header the edges send to the origin with origin_auth_secret as its value (needs the secret)"
              }
            }
          },
          "origin_auth_secret": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 512,
            "description": "Write only, sealed at rest, never returned (`origin_auth_set`); \"\" or null removes it"
          },
          "log_export": {
            "type": "object",
            "description": "v3 hourly raw access logs (gzip NDJSON, the `edge_requests` raw export) to the tenant's S3 bucket or SFTP server; `{}` = off. Hours before enabling are never exported; deliveries are listed by `GET /v1/distributions/{id}/log-deliveries`.",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "s3",
                  "sftp"
                ]
              },
              "endpoint": {
                "type": "string",
                "description": "s3: https endpoint (empty = AWS)"
              },
              "region": {
                "type": "string"
              },
              "bucket": {
                "type": "string"
              },
              "prefix": {
                "type": "string",
                "description": "s3 key prefix; objects are `<prefix>/<hostname>/dt=YYYY-MM-DD/hour=HH.ndjson.gz`"
              },
              "host": {
                "type": "string",
                "description": "sftp host"
              },
              "port": {
                "type": "integer",
                "description": "sftp port (default 22)"
              },
              "user": {
                "type": "string"
              },
              "dir": {
                "type": "string",
                "description": "sftp directory (absolute); files are `<dir>/<hostname>-YYYYMMDDHH.ndjson.gz`"
              },
              "host_key": {
                "type": "string",
                "description": "sftp server key (authorized_keys format); empty = recorded on first connection"
              },
              "include_pii": {
                "type": "boolean",
                "description": "Viewer-level fields (IP, user agent); needs the stats:pii permission"
              },
              "enabled_at": {
                "type": "string",
                "format": "date-time",
                "readOnly": true
              }
            }
          },
          "log_export_credentials": {
            "type": "object",
            "writeOnly": true,
            "description": "Sealed at rest, never returned (`log_export_has_credentials`): s3 access_key + secret_key, sftp password or private_key",
            "properties": {
              "access_key": {
                "type": "string"
              },
              "secret_key": {
                "type": "string"
              },
              "password": {
                "type": "string"
              },
              "private_key": {
                "type": "string"
              }
            }
          },
          "enabled": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "Distribution": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DistributionInput"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "customer_id": {
                "type": "string",
                "format": "uuid"
              },
              "tenant_mode": {
                "type": "string",
                "enum": [
                  "platform",
                  "cdn",
                  "ai",
                  "drm"
                ]
              },
              "primary": {
                "type": "boolean",
                "description": "the tenant's customers.cdn_hostname (manifests, pre-warm); cannot be deleted or disabled"
              },
              "secret_hint": {
                "type": "string",
                "description": "First 6 characters of the current secret"
              },
              "secret": {
                "type": "string",
                "description": "create / rotate / token-mode switch responses only — shown once"
              },
              "prev_secret_active": {
                "type": "boolean"
              },
              "token_secret_prev_until": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "cname_target": {
                "type": "string"
              },
              "edge_ips": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "origin_auth_set": {
                "type": "boolean",
                "description": "An origin auth secret is stored (never returned)"
              },
              "log_export_has_credentials": {
                "type": "boolean",
                "description": "Log export credentials are stored (never returned)"
              },
              "cache_generation": {
                "type": "integer",
                "description": "Bumped by a distribution purge; part of every edge cache key when > 0"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "StatsRealtime": {
        "type": "object",
        "properties": {
          "concurrent": {
            "type": "object",
            "properties": {
              "live": {
                "type": "integer"
              },
              "vod": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "plays_last_5m": {
            "type": "integer"
          },
          "plays_last_hour": {
            "type": "integer"
          },
          "gbps": {
            "type": "object",
            "properties": {
              "total": {
                "type": "number"
              },
              "by_site": {
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                }
              }
            }
          },
          "top_channels": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "slug": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "concurrent": {
                  "type": "integer"
                }
              }
            }
          },
          "top_assets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "concurrent": {
                  "type": "integer"
                }
              }
            }
          },
          "fatal_errors_per_min": {
            "type": "integer"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "StatsOverviewValues": {
        "type": "object",
        "properties": {
          "plays": {
            "type": "integer"
          },
          "attempts": {
            "type": "integer"
          },
          "ebvs_pct": {
            "type": "number"
          },
          "viewers": {
            "type": "integer"
          },
          "watch_time_ms": {
            "type": "integer"
          },
          "avg_view_duration_ms": {
            "type": "number"
          },
          "completion_rate": {
            "type": "number"
          },
          "peak_concurrent": {
            "type": "integer"
          },
          "peak_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "startup_p50_ms": {
            "type": "number"
          },
          "startup_p95_ms": {
            "type": "number"
          },
          "rebuffer_ratio": {
            "type": "number"
          },
          "error_rate": {
            "type": "number"
          },
          "avg_bitrate_kbps": {
            "type": "number"
          },
          "ad_impressions": {
            "type": "integer"
          },
          "vid_coverage": {
            "type": "number",
            "description": "share of plays with a stable anonymous viewer id; Studio labels viewers approximate below 0.8"
          },
          "plays_no_consent": {
            "type": "integer",
            "description": "plays without consent (GPC/DNT, banner declined, tenant consent not given): one per session, never counted as viewers"
          },
          "ad_completion_rate": {
            "type": "number"
          },
          "bytes_delivered": {
            "type": "integer"
          }
        }
      },
      "StatsOverview": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "compare": {
            "type": "string",
            "enum": [
              "previous",
              "yesterday",
              "last_week"
            ]
          },
          "current": {
            "$ref": "#/components/schemas/StatsOverviewValues"
          },
          "previous": {
            "$ref": "#/components/schemas/StatsOverviewValues"
          }
        }
      },
      "StatsSeries": {
        "type": "object",
        "properties": {
          "metric": {
            "type": "string"
          },
          "interval": {
            "type": "string"
          },
          "group_by": {
            "type": "string"
          },
          "points": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "group": {
                  "type": "string"
                },
                "value": {
                  "type": "number"
                }
              }
            }
          },
          "compare": {
            "type": "string",
            "enum": [
              "previous",
              "yesterday",
              "last_week"
            ]
          },
          "previous_from": {
            "type": "string",
            "format": "date-time"
          },
          "previous_to": {
            "type": "string",
            "format": "date-time"
          },
          "offset_ms": {
            "type": "integer",
            "description": "How far the comparison points were shifted forward"
          },
          "previous": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "group": {
                  "type": "string"
                },
                "value": {
                  "type": "number"
                }
              }
            }
          }
        }
      },
      "StatsTraffic": {
        "type": "object",
        "properties": {
          "group_by": {
            "type": "string"
          },
          "interval": {
            "type": "string"
          },
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "group": {
                  "type": "string"
                },
                "requests": {
                  "type": "integer"
                },
                "bytes": {
                  "type": "integer"
                },
                "gbps": {
                  "type": "number"
                },
                "hit_ratio_requests": {
                  "type": "number"
                },
                "hit_ratio_bytes": {
                  "type": "number"
                },
                "origin_bytes": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "StatsSession": {
        "type": "object",
        "properties": {
          "sid": {
            "type": "string"
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "asset": {
            "type": "string"
          },
          "clip": {
            "type": "string"
          },
          "channel": {
            "type": "string"
          },
          "live": {
            "type": "boolean"
          },
          "watched_ms": {
            "type": "integer"
          },
          "startup_ms": {
            "type": "integer"
          },
          "rebuffers": {
            "type": "integer"
          },
          "rebuffer_ms": {
            "type": "integer"
          },
          "errors": {
            "type": "integer"
          },
          "fatal": {
            "type": "boolean"
          },
          "completed": {
            "type": "boolean"
          },
          "avg_bitrate_kbps": {
            "type": "number"
          },
          "country": {
            "type": "string"
          },
          "isp": {
            "type": "string"
          },
          "device": {
            "type": "string"
          },
          "os": {
            "type": "string"
          },
          "browser": {
            "type": "string"
          },
          "pathway": {
            "type": "string"
          },
          "page_host": {
            "type": "string"
          },
          "vid": {
            "type": "string",
            "description": "only with scope stats:pii"
          }
        }
      },
      "StatsSessionDetail": {
        "description": "One session with its player event timeline (GET /v1/stats/sessions/{sid})",
        "allOf": [
          {
            "$ref": "#/components/schemas/StatsSession"
          },
          {
            "type": "object",
            "properties": {
              "events": {
                "type": "array",
                "maxItems": 5000,
                "items": {
                  "type": "object",
                  "properties": {
                    "t": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "e": {
                      "type": "string",
                      "description": "Event type (session_start, first_frame, heartbeat, rebuffer, level_switch, seek, error, ended, ad events…)"
                    },
                    "fields": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "The event's non-empty fields: startup_ms, level_kbps, level_h, watched_ms, pos_ms, buf_ms, ms, from_kbps, to_kbps, from_ms, to_ms, reason, code, fatal, detail, ad_id, ad_pos"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "StatsBreakdown": {
        "type": "object",
        "properties": {
          "dimension": {
            "type": "string"
          },
          "metric": {
            "type": "string"
          },
          "coverage": {
            "type": "number",
            "description": "Viewer attributes only: share of plays that carry the attribute"
          },
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "description": "Dimension value ('' = not attributed / unknown)"
                },
                "value": {
                  "type": "number"
                },
                "share": {
                  "type": "number",
                  "description": "Share of the listed rows' total"
                },
                "title": {
                  "type": "string",
                  "description": "programme dimension: programme title"
                },
                "start": {
                  "type": "string",
                  "format": "date-time",
                  "description": "programme dimension: programme start"
                },
                "channel": {
                  "type": "string",
                  "description": "programme dimension: channel slug"
                }
              }
            }
          }
        }
      },
      "StatsTop": {
        "type": "object",
        "properties": {
          "entity": {
            "type": "string",
            "enum": [
              "assets",
              "clips",
              "channels"
            ]
          },
          "metric": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Asset or clip id, or channel slug"
                },
                "title": {
                  "type": "string",
                  "description": "Title from the catalogue",
                  "when known": null
                },
                "value": {
                  "type": "number"
                }
              }
            }
          }
        }
      },
      "StatsTrafficPercentile": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "sample_secs": {
            "type": "integer",
            "enum": [
              300,
              3600
            ],
            "description": "Sample length (1-hour samples when the range starts beyond the 1-minute retention)"
          },
          "samples": {
            "type": "integer"
          },
          "p95_gbps": {
            "type": "number"
          },
          "peak_gbps": {
            "type": "number"
          },
          "mean_gbps": {
            "type": "number"
          }
        }
      },
      "StatsRecurrence": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "lookback_days": {
            "type": "integer"
          },
          "viewers": {
            "type": "integer",
            "description": "new + returning + without_vid"
          },
          "new": {
            "type": "integer"
          },
          "returning": {
            "type": "integer"
          },
          "without_vid": {
            "type": "integer",
            "description": "Viewers without a stable id"
          },
          "plays_no_consent": {
            "type": "integer",
            "description": "Plays without consent — not viewers, neither new nor returning"
          },
          "vid_coverage": {
            "type": "number",
            "description": "Share of plays that carry a stable viewer id"
          },
          "approximate": {
            "type": "boolean",
            "description": "vid_coverage below 0.8"
          }
        }
      },
      "StatsDenials": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "interval": {
            "type": "string",
            "enum": [
              "1m",
              "1h",
              "1d"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Denied requests over the listed reasons"
          },
          "reasons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsDenialRow"
            }
          },
          "countries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsDenialRow"
            }
          },
          "asns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsDenialRow"
            }
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "reason": {
                  "type": "string"
                },
                "requests": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "StatsDenialRow": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "requests": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          }
        }
      },
      "StatsSiteReport": {
        "type": "object",
        "description": "GET /v1/stats/sites",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "interval": {
            "type": "string"
          },
          "hosts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "page_views": {
            "type": "integer"
          },
          "visitors": {
            "type": "integer"
          },
          "sessions": {
            "type": "integer"
          },
          "tile_clicks": {
            "type": "integer"
          },
          "searches": {
            "type": "integer"
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "views": {
                  "type": "integer"
                },
                "visitors": {
                  "type": "integer"
                }
              }
            }
          },
          "top_pages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "views": {
                  "type": "integer"
                },
                "visitors": {
                  "type": "integer"
                }
              }
            }
          },
          "sections": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "section": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "views": {
                  "type": "integer"
                },
                "clicks": {
                  "type": "integer"
                },
                "ctr": {
                  "type": "number"
                }
              }
            }
          },
          "search_terms": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "query": {
                  "type": "string"
                },
                "searches": {
                  "type": "integer"
                },
                "zero_results": {
                  "type": "integer"
                }
              }
            }
          },
          "referrers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "host": {
                  "type": "string"
                },
                "views": {
                  "type": "integer"
                }
              }
            }
          },
          "positions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "position": {
                  "type": "integer"
                },
                "clicks": {
                  "type": "integer"
                },
                "share": {
                  "type": "number",
                  "description": "of all clicks with a known position"
                },
                "ctr": {
                  "type": "number",
                  "description": "clicks at this position per section view"
                }
              }
            }
          },
          "section_views": {
            "type": "integer"
          },
          "position_unknown": {
            "type": "integer"
          },
          "not_found": {
            "type": "integer"
          },
          "not_found_pages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "hits": {
                  "type": "integer"
                },
                "visitors": {
                  "type": "integer"
                },
                "referrer": {
                  "type": "string",
                  "description": "Most frequent other-site referrer ('' = typed, bookmarked or own-site link)"
                },
                "internal": {
                  "type": "integer",
                  "description": "Hits from a link on the site itself"
                }
              }
            }
          },
          "not_found_since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "StatsAdRow": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Group value (absent in totals)"
          },
          "requests": {
            "type": "integer"
          },
          "impressions": {
            "type": "integer"
          },
          "starts": {
            "type": "integer"
          },
          "q1": {
            "type": "integer"
          },
          "mid": {
            "type": "integer"
          },
          "q3": {
            "type": "integer"
          },
          "completes": {
            "type": "integer"
          },
          "skips": {
            "type": "integer"
          },
          "clicks": {
            "type": "integer"
          },
          "errors": {
            "type": "integer"
          },
          "fill_rate": {
            "type": "number"
          },
          "completion_rate": {
            "type": "number"
          },
          "skip_rate": {
            "type": "number"
          },
          "ctr": {
            "type": "number"
          }
        }
      },
      "StatsAds": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "group_by": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "live",
              "test",
              "all"
            ]
          },
          "totals": {
            "$ref": "#/components/schemas/StatsAdRow"
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsAdRow"
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "description": "VAST error code"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "interval": {
            "type": "string"
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "requests": {
                  "type": "integer"
                },
                "impressions": {
                  "type": "integer"
                },
                "completes": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "StatsViewerErasure": {
        "type": "object",
        "properties": {
          "customer": {
            "type": "string",
            "description": "Tenant slug"
          },
          "viewer_hash": {
            "type": "string",
            "description": "This year's stored (hashed) form of the id — the reference support keeps; the raw id is never stored or logged"
          },
          "forms": {
            "type": "integer",
            "description": "Stored forms searched (stable + page hash for last, this and next year, plus the raw id for rows from before 2026-09-29)"
          },
          "rows": {
            "type": "integer",
            "format": "int64"
          },
          "ms": {
            "type": "integer",
            "format": "int64",
            "description": "Time the deletion took (lightweight DELETE, synchronous)"
          },
          "tables": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "table": {
                  "type": "string"
                },
                "rows": {
                  "type": "integer",
                  "format": "int64"
                },
                "ms": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          },
          "suppressed": {
            "type": "boolean",
            "description": "insight blanks this id on ingest from now on (within a minute): it is neither stored nor exported again"
          }
        }
      },
      "StatsProgrammeReport": {
        "type": "object",
        "properties": {
          "channel": {
            "type": "string",
            "description": "Channel slug"
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "programmes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "title": {
                  "type": "string"
                },
                "start": {
                  "type": "string",
                  "format": "date-time"
                },
                "end": {
                  "type": "string",
                  "format": "date-time"
                },
                "series_id": {
                  "type": "string"
                },
                "series_title": {
                  "type": "string"
                },
                "avg_concurrent": {
                  "type": "number",
                  "description": "Mean per-minute concurrent viewers over the minutes that have passed (no sample = 0)"
                },
                "peak_concurrent": {
                  "type": "integer"
                },
                "peak_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "minutes": {
                  "type": "integer",
                  "description": "Minutes of the programme up to now"
                },
                "minutes_with_data": {
                  "type": "integer"
                },
                "viewers": {
                  "type": "integer",
                  "description": "Distinct viewers who watched any part"
                },
                "plays": {
                  "type": "integer",
                  "description": "Plays that started during it"
                },
                "on_air": {
                  "type": "boolean"
                }
              }
            }
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "description": "Series id, or title:<title> for programmes without a show"
                },
                "title": {
                  "type": "string"
                },
                "programmes": {
                  "type": "integer"
                },
                "avg_concurrent": {
                  "type": "number",
                  "description": "Minute-weighted over its programmes"
                },
                "peak_concurrent": {
                  "type": "integer"
                },
                "viewers": {
                  "type": "integer",
                  "description": "Sum over programmes (a viewer of two episodes counts twice)"
                },
                "plays": {
                  "type": "integer"
                }
              }
            }
          },
          "data_since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "First minute of the channel's curve in the range (null = no samples)"
          }
        }
      },
      "StatsSmoothnessRow": {
        "type": "object",
        "required": [
          "score",
          "sessions",
          "scored",
          "exits_before_start",
          "fatal",
          "rebuffer_ratio",
          "startup_p50_ms",
          "switches_per_hour",
          "watch_time_ms"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "the group value (absent on the total)"
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "watch-time-weighted smoothness; null = no scored session"
          },
          "sessions": {
            "type": "integer"
          },
          "scored": {
            "type": "integer",
            "description": "sessions with a first frame or a fatal error"
          },
          "exits_before_start": {
            "type": "integer",
            "description": "left before the first frame without an error — not scored"
          },
          "fatal": {
            "type": "integer"
          },
          "rebuffer_ratio": {
            "type": "number"
          },
          "startup_p50_ms": {
            "type": [
              "number",
              "null"
            ]
          },
          "switches_per_hour": {
            "type": "number"
          },
          "watch_time_ms": {
            "type": "integer"
          }
        }
      },
      "StatsSmoothness": {
        "type": "object",
        "required": [
          "from",
          "to",
          "interval",
          "formula",
          "total",
          "rows",
          "series"
        ],
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "interval": {
            "type": "string"
          },
          "group_by": {
            "type": "string"
          },
          "formula": {
            "type": "string",
            "description": "how the score is computed"
          },
          "total": {
            "$ref": "#/components/schemas/StatsSmoothnessRow"
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsSmoothnessRow"
            }
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "t",
                "score",
                "scored"
              ],
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "score": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "scored": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "StatsQoE": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "interval": {
            "type": "string"
          },
          "sessions": {
            "type": "integer",
            "description": "Play attempts (player sessions)"
          },
          "plays": {
            "type": "integer",
            "description": "Sessions that reached a first frame"
          },
          "startup_ms": {
            "type": [
              "object",
              "null"
            ],
            "description": "Startup time percentiles in ms (null = no play measured one)",
            "properties": {
              "p50": {
                "type": "number"
              },
              "p75": {
                "type": "number"
              },
              "p90": {
                "type": "number"
              },
              "p95": {
                "type": "number"
              },
              "p99": {
                "type": "number"
              },
              "measured": {
                "type": "integer"
              }
            }
          },
          "rebuffer_ratio": {
            "type": "number"
          },
          "rebuffers": {
            "type": "integer"
          },
          "rebuffer_ms": {
            "type": "integer"
          },
          "watch_time_ms": {
            "type": "integer"
          },
          "rebuffers_per_hour": {
            "type": "number"
          },
          "error_rate": {
            "type": "number"
          },
          "fatal_sessions": {
            "type": "integer"
          },
          "errors": {
            "type": "integer",
            "description": "Error events (fatal or not)"
          },
          "switches": {
            "type": "integer"
          },
          "switches_per_play": {
            "type": "number"
          },
          "switches_per_hour": {
            "type": "number"
          },
          "avg_bitrate_kbps": {
            "type": [
              "number",
              "null"
            ],
            "description": "Watch-time weighted; null = no heartbeat carried a bitrate"
          },
          "renditions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "height": {
                  "type": "integer"
                },
                "kbps": {
                  "type": "integer"
                },
                "watch_time_ms": {
                  "type": "integer"
                },
                "share": {
                  "type": "number"
                }
              }
            }
          },
          "rendition_source": {
            "type": "string"
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "plays": {
                  "type": "integer"
                },
                "startup_p50_ms": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "startup_p95_ms": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "rebuffer_ratio": {
                  "type": "number"
                },
                "error_rate": {
                  "type": "number"
                },
                "switches_per_play": {
                  "type": "number"
                },
                "avg_bitrate_kbps": {
                  "type": [
                    "number",
                    "null"
                  ]
                }
              }
            }
          },
          "data_since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "StatsCDNTotals": {
        "type": "object",
        "properties": {
          "requests": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "hit_ratio_requests": {
            "type": "number"
          },
          "hit_ratio_bytes": {
            "type": "number"
          },
          "origin_bytes": {
            "type": "integer"
          },
          "avg_request_ms": {
            "type": [
              "number",
              "null"
            ]
          },
          "gbps": {
            "type": "number",
            "description": "Average over the range"
          }
        }
      },
      "StatsCDN": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "interval": {
            "type": "string"
          },
          "totals": {
            "$ref": "#/components/schemas/StatsCDNTotals"
          },
          "edges": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/StatsCDNTotals"
                },
                {
                  "type": "object",
                  "properties": {
                    "site": {
                      "type": "string"
                    },
                    "edge": {
                      "type": "string"
                    },
                    "share": {
                      "type": "number",
                      "description": "of delivered bytes"
                    }
                  }
                }
              ]
            }
          },
          "cache_status": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsCDNGroup"
            }
          },
          "status_class": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsCDNGroup"
            }
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "gbps": {
                  "type": "number"
                },
                "hit_ratio_requests": {
                  "type": "number"
                },
                "hit_ratio_bytes": {
                  "type": "number"
                },
                "requests": {
                  "type": "integer"
                }
              }
            }
          },
          "content_filter_ignored": {
            "type": "boolean"
          },
          "data_since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "StatsCDNGroup": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "requests": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "share": {
            "type": "number",
            "description": "of requests"
          }
        }
      },
      "StatsCompletionRow": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Asset / clip / programme id (absent in total)"
          },
          "title": {
            "type": "string"
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "channel": {
            "type": "string"
          },
          "plays": {
            "type": "integer",
            "description": "On-demand plays with a known duration"
          },
          "q25": {
            "type": "integer"
          },
          "q50": {
            "type": "integer"
          },
          "q75": {
            "type": "integer"
          },
          "q100": {
            "type": "integer"
          },
          "q25_rate": {
            "type": "number"
          },
          "q50_rate": {
            "type": "number"
          },
          "q75_rate": {
            "type": "number"
          },
          "q100_rate": {
            "type": "number"
          },
          "watch_time_ms": {
            "type": "integer"
          }
        }
      },
      "StatsCompletion": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "group_by": {
            "type": "string",
            "enum": [
              "asset",
              "clip",
              "programme"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "sessions",
              "rollup_1d"
            ]
          },
          "total": {
            "$ref": "#/components/schemas/StatsCompletionRow"
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsCompletionRow"
            }
          }
        }
      },
      "PrewarmRequest": {
        "type": "object",
        "properties": {
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "clip_id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "maxItems": 500
          }
        }
      },
      "PrewarmRun": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "trigger": {
            "type": "string",
            "enum": [
              "asset_ready",
              "clip_ready",
              "cms_publish",
              "manual",
              "channel_start"
            ]
          },
          "target": {
            "type": [
              "string",
              "null"
            ],
            "example": "asset:0192a1b2-0000-7000-8000-000000000001"
          },
          "urls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "partial",
              "failed"
            ]
          },
          "results": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "object",
              "properties": {
                "edge": {
                  "type": "string"
                },
                "site": {
                  "type": "string",
                  "description": "The edge's site"
                },
                "ok": {
                  "type": "integer"
                },
                "failed": {
                  "type": "integer"
                },
                "hits": {
                  "type": "integer"
                },
                "ms": {
                  "type": "integer"
                },
                "errors": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "sites": {
            "type": "array",
            "description": "Per-site completion, from results; empty until the run finished",
            "items": {
              "type": "object",
              "properties": {
                "site": {
                  "type": "string"
                },
                "edges": {
                  "type": "integer"
                },
                "ok": {
                  "type": "integer",
                  "description": "URLs fetched"
                },
                "failed": {
                  "type": "integer"
                },
                "complete": {
                  "type": "boolean",
                  "description": "Every edge of the site fetched every URL"
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "TeamMember": {
        "type": "object",
        "required": [
          "id",
          "email",
          "name",
          "role",
          "status",
          "last_login_at",
          "joined_at",
          "you"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "viewer",
              "editor",
              "publisher",
              "engineer",
              "admin",
              "owner"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "last_login_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "joined_at": {
            "type": "string",
            "format": "date-time"
          },
          "you": {
            "type": "boolean",
            "description": "The row of the calling user"
          }
        }
      },
      "PendingInvitation": {
        "type": "object",
        "required": [
          "id",
          "email",
          "role",
          "invited_by",
          "expires_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "viewer",
              "editor",
              "publisher",
              "engineer",
              "admin",
              "owner"
            ]
          },
          "invited_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The inviting user; null when an API key invited"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InvitationInput": {
        "type": "object",
        "required": [
          "email",
          "role"
        ],
        "additionalProperties": false,
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Trimmed and lower-cased"
          },
          "role": {
            "type": "string",
            "enum": [
              "viewer",
              "editor",
              "publisher",
              "engineer",
              "admin",
              "owner"
            ],
            "description": "owner only when the caller is an owner's console session (never an API key)"
          }
        }
      },
      "InvitationCreated": {
        "type": "object",
        "required": [
          "invitation"
        ],
        "properties": {
          "invitation": {
            "type": "object",
            "required": [
              "id",
              "customer_id",
              "email",
              "role",
              "invited_by",
              "expires_at",
              "accepted_at",
              "revoked_at",
              "created_at"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "customer_id": {
                "type": "string",
                "format": "uuid",
                "description": "The tenant"
              },
              "email": {
                "type": "string"
              },
              "role": {
                "type": "string",
                "enum": [
                  "viewer",
                  "editor",
                  "publisher",
                  "engineer",
                  "admin",
                  "owner"
                ]
              },
              "invited_by": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time"
              },
              "accepted_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "revoked_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "AuditEntry": {
        "type": "object",
        "required": [
          "id",
          "at",
          "actor",
          "tenant_id",
          "action",
          "target",
          "data"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Monotonic row id (the keyset cursor)"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "actor": {
            "type": "string",
            "description": "user:<email>, key:<prefix>, console or interhost:<email> as <tenant>"
          },
          "tenant_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "action": {
            "type": "string",
            "description": "e.g. asset.create, team.role_change, auth.login"
          },
          "target": {
            "type": [
              "string",
              "null"
            ]
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "BillingStatementLine": {
        "type": "object",
        "required": [
          "metric",
          "quantity",
          "unit",
          "unit_price",
          "amount",
          "status"
        ],
        "properties": {
          "metric": {
            "type": "string",
            "description": "base_monthly, p95_mbps, delivered_tb, partner_cdn_gb, storage_recordings_gb, storage_vod_gb, storage_images_gb, transcode_minutes, gpu_minutes, live_channel_days, dual_recording_channel_days, site_page_views, export_events, ssai_impressions_k, drm_licences_k, drm_keys (only for tenants with stitched ads or a price for them)"
          },
          "quantity": {
            "type": "string",
            "description": "Decimal string"
          },
          "unit": {
            "type": "string",
            "description": "month, Mbps, TB, GB, min, channel-days, views, events"
          },
          "unit_price": {
            "type": [
              "string",
              "null"
            ]
          },
          "amount": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "no_data",
              "unavailable"
            ]
          },
          "note": {
            "type": "string"
          }
        }
      },
      "BillingStatement": {
        "type": "object",
        "description": "A monthly usage statement (not a tax invoice; amounts are decimal strings before VAT)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenant_id": {
            "type": "string",
            "format": "uuid"
          },
          "tenant_slug": {
            "type": "string"
          },
          "tenant_name": {
            "type": "string"
          },
          "month": {
            "type": "string",
            "pattern": "^[0-9]{4}-[0-9]{2}$"
          },
          "status": {
            "type": "string",
            "description": "Always approved in the tenant API"
          },
          "currency": {
            "type": "string"
          },
          "price_list_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "price_list_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "timezone": {
            "type": "string"
          },
          "period_from": {
            "type": "string",
            "format": "date-time"
          },
          "period_to": {
            "type": "string",
            "format": "date-time"
          },
          "partial": {
            "type": "boolean",
            "description": "The month was not complete when the statement was generated"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BillingStatementLine"
            }
          },
          "credits": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "amount": {
                  "type": "string",
                  "description": "Subtracted from the subtotal"
                }
              }
            }
          },
          "subtotal": {
            "type": "string"
          },
          "credits_total": {
            "type": "string"
          },
          "total": {
            "type": "string"
          },
          "p95_mbps": {
            "type": [
              "string",
              "null"
            ]
          },
          "commit_mbps": {
            "type": [
              "string",
              "null"
            ]
          },
          "usage": {
            "type": "null",
            "description": "Always null in the tenant API"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_by": {
            "type": "null",
            "description": "Always null in the tenant API"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "approved_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "approved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "voided_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "voided_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "void_reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "NotificationRuleInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "events": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": [
                "feed.lost",
                "feed.restored",
                "ingest.failover",
                "recording.redundancy",
                "asset.failed",
                "asset.ready",
                "storage.warning",
                "epg.delivery_failed",
                "*"
              ]
            }
          },
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "telegram"
            ]
          },
          "recipients": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "items": {
              "type": "string"
            },
            "description": "Emails of active members of the tenant (email) or numeric Telegram chat ids, `-` prefix for groups (telegram)"
          },
          "enabled": {
            "type": "boolean",
            "description": "Default true on create"
          }
        }
      },
      "ReportSchedule": {
        "type": "object",
        "description": "change scheduled-reports: a tenant's scheduled statistics report (daily 08:00 / weekly Sunday 08:00, Israel time)",
        "required": [
          "id",
          "name",
          "cadence",
          "lang",
          "paused",
          "next_run_at",
          "recipients",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "cadence": {
            "type": "string",
            "enum": [
              "daily",
              "weekly"
            ]
          },
          "lang": {
            "type": "string",
            "enum": [
              "he",
              "en"
            ]
          },
          "paused": {
            "type": "boolean"
          },
          "attach_pdf": {
            "type": "boolean",
            "description": "report.pdf is attached to the mail (\"צרף PDF\"; default true). A PDF that fails or times out is left out and noted in the send's error; the mail still goes"
          },
          "next_run_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_run_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "sent",
              "partial",
              "failed",
              "no_recipients",
              null
            ]
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_trigger": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "scheduled",
              "manual",
              null
            ]
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "recipients": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportRecipient"
            }
          }
        }
      },
      "ReportRecipient": {
        "type": "object",
        "required": [
          "id",
          "email",
          "kind",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "user_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "member",
              "external"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "unsubscribed"
            ]
          },
          "confirm_sent_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "confirmed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "unsubscribed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ReportScheduleInput": {
        "type": "object",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "cadence": {
            "type": "string",
            "enum": [
              "daily",
              "weekly"
            ],
            "default": "daily"
          },
          "lang": {
            "type": "string",
            "enum": [
              "he",
              "en"
            ],
            "default": "he"
          },
          "paused": {
            "type": "boolean",
            "default": false
          },
          "attach_pdf": {
            "type": "boolean",
            "default": true
          },
          "members": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "active team members (user ids)"
          },
          "emails": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            },
            "description": "explicit addresses — each gets a confirmation mail first"
          }
        }
      },
      "ReportSend": {
        "type": "object",
        "required": [
          "id",
          "schedule_id",
          "trigger",
          "period_from",
          "period_to",
          "status",
          "recipients",
          "failed",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "schedule_id": {
            "type": "string",
            "format": "uuid"
          },
          "trigger": {
            "type": "string",
            "enum": [
              "scheduled",
              "manual"
            ]
          },
          "period_from": {
            "type": "string",
            "format": "date-time"
          },
          "period_to": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "sent",
              "partial",
              "failed",
              "no_recipients"
            ]
          },
          "recipients": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "per-recipient errors with masked addresses"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SupportRequest": {
        "type": "object",
        "description": "A support request. `assignee` is null in tenant answers.",
        "required": [
          "id",
          "tenant_id",
          "tenant_slug",
          "tenant_name",
          "created_by",
          "contact_email",
          "contact_name",
          "lang",
          "category",
          "priority",
          "subject",
          "message",
          "context",
          "status",
          "created_at",
          "updated_at",
          "message_count"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenant_id": {
            "type": "string",
            "format": "uuid"
          },
          "tenant_slug": {
            "type": "string"
          },
          "tenant_name": {
            "type": "string"
          },
          "created_by_user": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_by_key": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_by": {
            "type": "string",
            "description": "Audit actor (user:<email> or key:<prefix>)"
          },
          "contact_email": {
            "type": "string",
            "format": "email"
          },
          "contact_name": {
            "type": "string"
          },
          "lang": {
            "type": "string",
            "enum": [
              "he",
              "en"
            ],
            "description": "Language of the mails to the requester"
          },
          "category": {
            "type": "string",
            "enum": [
              "question",
              "bug",
              "outage",
              "billing",
              "feature_request",
              "other"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "normal",
              "high",
              "urgent"
            ]
          },
          "subject": {
            "type": "string",
            "maxLength": 200
          },
          "message": {
            "type": "string",
            "maxLength": 10000
          },
          "context": {
            "$ref": "#/components/schemas/SupportContext"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "in_progress",
              "waiting_customer",
              "resolved",
              "closed"
            ]
          },
          "assignee": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_reply_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "message_count": {
            "type": "integer"
          }
        }
      },
      "SupportMessage": {
        "type": "object",
        "required": [
          "id",
          "request_id",
          "author_kind",
          "author_email",
          "author_name",
          "body",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          },
          "author_kind": {
            "type": "string",
            "enum": [
              "customer",
              "interhost"
            ]
          },
          "author_email": {
            "type": "string",
            "description": "Empty for Interhost authors in tenant answers"
          },
          "author_name": {
            "type": "string"
          },
          "body": {
            "type": "string",
            "maxLength": 10000
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SupportRequestDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SupportRequest"
          },
          {
            "type": "object",
            "required": [
              "messages"
            ],
            "properties": {
              "messages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SupportMessage"
                },
                "description": "The conversation after the opening message",
                "oldest first": null
              }
            }
          }
        ]
      },
      "SupportRequestPage": {
        "type": "object",
        "required": [
          "items",
          "next_cursor",
          "counts"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SupportRequest"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "counts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Requests per status"
          }
        }
      },
      "SupportContext": {
        "type": "object",
        "additionalProperties": false,
        "description": "What Studio attaches to a request; every field optional and bounded by the server.",
        "properties": {
          "page_path": {
            "type": "string",
            "maxLength": 500,
            "description": "Path only (query string and fragment dropped)"
          },
          "studio_version": {
            "type": "string",
            "maxLength": 64
          },
          "user_agent": {
            "type": "string",
            "maxLength": 500
          },
          "time_zone": {
            "type": "string",
            "maxLength": 64
          },
          "locale": {
            "type": "string",
            "maxLength": 16
          },
          "request_ids": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "string",
              "maxLength": 100
            }
          },
          "errors": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "at": {
                  "type": "string"
                },
                "method": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "status": {
                  "type": "integer"
                },
                "type": {
                  "type": "string"
                },
                "message": {
                  "type": "string",
                  "maxLength": 1000
                },
                "request_id": {
                  "type": "string"
                }
              }
            }
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "asset_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "SupportRequestInput": {
        "type": "object",
        "required": [
          "category",
          "subject",
          "message"
        ],
        "additionalProperties": false,
        "properties": {
          "category": {
            "type": "string",
            "enum": [
              "question",
              "bug",
              "outage",
              "billing",
              "feature_request",
              "other"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "normal",
              "high",
              "urgent"
            ],
            "default": "normal"
          },
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "contact_email": {
            "type": "string",
            "format": "email"
          },
          "contact_name": {
            "type": "string",
            "maxLength": 200
          },
          "lang": {
            "type": "string",
            "enum": [
              "he",
              "en"
            ]
          },
          "context": {
            "$ref": "#/components/schemas/SupportContext"
          }
        }
      },
      "StatsExport": {
        "type": "object",
        "required": [
          "id",
          "delivery",
          "report",
          "format",
          "from",
          "to",
          "filters",
          "pii",
          "status",
          "truncated",
          "email",
          "created_at",
          "file_name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "delivery": {
            "type": "string",
            "enum": [
              "file",
              "stream"
            ]
          },
          "report": {
            "type": "string",
            "enum": [
              "sessions",
              "player_events",
              "edge_requests",
              "site_events",
              "programmes",
              "qoe_mux"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "csv",
              "ndjson"
            ]
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "filters": {
            "type": "object",
            "additionalProperties": true
          },
          "pii": {
            "type": "boolean",
            "description": "made with stats:pii — viewer-level columns included; downloading needs stats:pii"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "ready",
              "failed",
              "expired"
            ]
          },
          "rows": {
            "type": [
              "integer",
              "null"
            ]
          },
          "truncated": {
            "type": "boolean",
            "description": "the file stops at the 1,000,000-row cap"
          },
          "bytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "gzip size"
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": "boolean",
            "description": "the requester is mailed when the file is ready"
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "file_name": {
            "type": "string",
            "example": "viewstream-tv10-sessions-20261004T0000Z-20261005T0000Z.csv.gz"
          },
          "download_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "signed link, no session needed, valid until download_expires_at"
          },
          "download_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "MonitorCheckDef": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "feed_down",
              "no_segments",
              "encoder_behind",
              "recorder_gap",
              "edge_errors",
              "black_video",
              "lipsync",
              "audio_silence",
              "video_frozen",
              "stale_playlists",
              "edge_5xx_rate",
              "edge_4xx_rate",
              "segment_time_p95",
              "cache_hit_ratio",
              "edge_reachable",
              "tls_expiry",
              "origin_fetch_p95",
              "service_status",
              "ai_services",
              "startup_p95",
              "rebuffer_ratio",
              "error_rate",
              "plays",
              "subtitles_failures",
              "translation_waiting",
              "epg_stale",
              "epg_coverage",
              "recording_leg_down",
              "recording_redundancy_lost",
              "ingest_feed_down",
              "ingest_failover"
            ]
          },
          "family": {
            "type": "string",
            "enum": [
              "live",
              "qoe",
              "delivery",
              "origin",
              "service"
            ]
          },
          "targets": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "channel",
                "asset",
                "site",
                "tenant"
              ]
            }
          },
          "op": {
            "type": "string",
            "enum": [
              "gt",
              "lt"
            ],
            "description": "gt: fires when the value is above the threshold; lt: below"
          },
          "unit": {
            "type": "string",
            "enum": [
              "s",
              "ms",
              "pct",
              "count",
              "min",
              "luma",
              "days",
              "level"
            ]
          },
          "threshold": {
            "type": "number"
          },
          "for_s": {
            "type": "integer"
          },
          "window_s": {
            "type": "integer"
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "critical"
            ]
          },
          "label_he": {
            "type": "string"
          },
          "label_en": {
            "type": "string"
          },
          "help_he": {
            "type": "string"
          },
          "help_en": {
            "type": "string"
          },
          "min_samples": {
            "type": "integer"
          }
        }
      },
      "MonitorCheck": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Unique within the monitor; defaults to the type"
          },
          "type": {
            "type": "string",
            "description": "A check type of GET /v1/monitors/checks"
          },
          "threshold": {
            "type": "number",
            "description": "In the check's unit (pct = percent, luma = 0–255)"
          },
          "for_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 86400,
            "description": "How long the condition must hold before it fires"
          },
          "window_s": {
            "type": "integer",
            "description": "QoE / edge / gap checks: the window evaluated (60–21600 s, whole minutes)"
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "critical"
            ]
          },
          "params": {
            "type": "object",
            "description": "plays: during_programme (boolean, channel monitors), baseline_plays (number); QoE/edge: min_samples; delivery/origin: host (one of your CDN hostnames); service_status: layer (any|streaming|delivery|origin); ai_services: service (any|subtitles|translation|upscale|lipsync); ingest_feed_down: feed (a|b); recording_leg_down: leg (1|2)"
          }
        }
      },
      "QuietHours": {
        "type": "object",
        "required": [
          "from",
          "to"
        ],
        "properties": {
          "from": {
            "type": "string",
            "example": "23:00"
          },
          "to": {
            "type": "string",
            "example": "07:00"
          },
          "tz": {
            "type": "string",
            "default": "Asia/Jerusalem"
          },
          "allow_critical": {
            "type": "boolean"
          }
        }
      },
      "MonitorInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "target_kind": {
            "type": "string",
            "enum": [
              "channel",
              "asset",
              "site",
              "tenant"
            ]
          },
          "target_id": {
            "type": "string",
            "format": "uuid",
            "description": "channel or asset id"
          },
          "target_host": {
            "type": "string",
            "description": "site: the host name the player runs on"
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MonitorCheck"
            }
          },
          "destinations": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "inapp": {
            "type": "boolean",
            "default": true
          },
          "notify_resolve": {
            "type": "boolean",
            "default": true
          },
          "repeat_min": {
            "type": "integer",
            "description": "0 = never, else 30–1440: re-send an unacknowledged firing alert"
          },
          "quiet_hours": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/QuietHours"
              },
              {
                "type": "null"
              }
            ]
          },
          "enabled": {
            "type": "boolean"
          }
        }
      },
      "MonitorCheckState": {
        "type": "object",
        "properties": {
          "check_key": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "ok",
              "pending",
              "firing",
              "nodata"
            ]
          },
          "since": {
            "type": "string",
            "format": "date-time"
          },
          "breach_since": {
            "type": "string",
            "format": "date-time"
          },
          "clear_since": {
            "type": "string",
            "format": "date-time"
          },
          "value": {
            "type": [
              "number",
              "null"
            ]
          },
          "detail": {
            "type": "object"
          },
          "alert_id": {
            "type": "string",
            "format": "uuid"
          },
          "evaluated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Monitor": {
        "type": "object",
        "required": [
          "id",
          "name",
          "target_kind",
          "checks",
          "destinations",
          "enabled"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "target_kind": {
            "type": "string",
            "enum": [
              "channel",
              "asset",
              "site",
              "tenant"
            ]
          },
          "target_id": {
            "type": "string",
            "format": "uuid"
          },
          "target_host": {
            "type": "string"
          },
          "target_label": {
            "type": "string"
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MonitorCheck"
            }
          },
          "destinations": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "inapp": {
            "type": "boolean"
          },
          "notify_resolve": {
            "type": "boolean"
          },
          "repeat_min": {
            "type": "integer"
          },
          "quiet_hours": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/QuietHours"
              },
              {
                "type": "null"
              }
            ]
          },
          "snoozed_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "snooze_reason": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "preset": {
            "type": "string"
          },
          "created_by": {
            "type": "string",
            "format": "uuid",
            "description": "The Studio user who created it (absent for API keys)"
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "pending",
              "firing",
              "muted",
              "nodata",
              "disabled"
            ]
          },
          "states": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MonitorCheckState"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AlertSend": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "destination_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "email",
              "telegram"
            ]
          },
          "event": {
            "type": "string",
            "enum": [
              "firing",
              "resolved",
              "repeat",
              "test",
              "confirm"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "sent",
              "failed",
              "skipped"
            ]
          },
          "reason": {
            "type": "string",
            "description": "failed/skipped: why (rate_limited, destination pending, firing was not sent, …)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Alert": {
        "type": "object",
        "required": [
          "id",
          "monitor_name",
          "check_key",
          "check_type",
          "severity",
          "title_he",
          "title_en",
          "started_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "monitor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "monitor_name": {
            "type": "string"
          },
          "check_key": {
            "type": "string"
          },
          "check_type": {
            "type": "string"
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "critical"
            ]
          },
          "target_kind": {
            "type": "string"
          },
          "target_id": {
            "type": "string",
            "format": "uuid"
          },
          "target_host": {
            "type": "string"
          },
          "target_label": {
            "type": "string"
          },
          "value": {
            "type": [
              "number",
              "null"
            ]
          },
          "threshold": {
            "type": [
              "number",
              "null"
            ]
          },
          "title_he": {
            "type": "string"
          },
          "title_en": {
            "type": "string"
          },
          "body_he": {
            "type": "string"
          },
          "body_en": {
            "type": "string"
          },
          "link": {
            "type": "string",
            "description": "The channel / asset / monitoring page in Studio"
          },
          "muted": {
            "type": "string",
            "enum": [
              "snoozed",
              "quiet_hours"
            ],
            "description": "Why nothing was sent when it fired"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "resolved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "resolve_note": {
            "type": "string"
          },
          "acked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "acked_by": {
            "type": "string",
            "format": "uuid"
          },
          "ack_note": {
            "type": "string"
          },
          "sends": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AlertSend"
            }
          }
        }
      },
      "AlertDestination": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "name",
          "target",
          "lang",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "email",
              "telegram"
            ]
          },
          "name": {
            "type": "string"
          },
          "target": {
            "type": "string",
            "description": "E-mail address or Telegram chat id (masked without notifications:manage)"
          },
          "lang": {
            "type": "string",
            "enum": [
              "he",
              "en"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "pending",
              "disabled"
            ],
            "description": "pending = the recipient has not confirmed yet"
          },
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "verify_sent_at": {
            "type": "string",
            "format": "date-time"
          },
          "verified_at": {
            "type": "string",
            "format": "date-time"
          },
          "disabled_reason": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NotificationRule": {
        "type": "object",
        "required": [
          "id",
          "customer_id",
          "name",
          "events",
          "channel",
          "recipients",
          "enabled",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "telegram"
            ]
          },
          "recipients": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "enabled": {
            "type": "boolean"
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Notification": {
        "type": "object",
        "required": [
          "id",
          "event_type",
          "severity",
          "title",
          "body",
          "data",
          "created_at",
          "read"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "event_type": {
            "type": "string",
            "description": "feed.lost | feed.restored | ingest.failover | recording.redundancy | asset.failed | asset.ready | storage.warning | epg.delivery_failed | alert.firing | alert.resolved | test"
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "critical"
            ]
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "description": "The source event's payload (or an empty object)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "read": {
            "type": "boolean",
            "description": "Read by the calling user (always false for an API key)"
          }
        }
      },
      "NotificationSend": {
        "type": "object",
        "required": [
          "id",
          "channel",
          "recipient",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "notification_id": {
            "type": "string",
            "format": "uuid"
          },
          "rule_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "channel": {
            "type": "string"
          },
          "recipient": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "sent",
              "failed"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EventDestination": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "https",
              "ga4",
              "s3",
              "kafka"
            ]
          },
          "url": {
            "type": "string",
            "description": "https: the endpoint; s3: the S3 endpoint; kafka: empty"
          },
          "config": {
            "$ref": "#/components/schemas/EventDestinationConfig"
          },
          "state": {
            "$ref": "#/components/schemas/EventDestinationState"
          },
          "format": {
            "type": "string",
            "enum": [
              "json",
              "ndjson"
            ]
          },
          "auth": {
            "type": "string",
            "enum": [
              "none",
              "bearer",
              "hmac"
            ]
          },
          "secret_hint": {
            "type": "string",
            "description": "hmac/bearer/ga4: tail of the secret; s3: tail of the access key; kafka: tail of the password"
          },
          "secret": {
            "type": "string",
            "description": "HMAC signing secret — create / rotate responses only"
          },
          "ga4_measurement_id": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "filters": {
            "$ref": "#/components/schemas/EventFilters"
          },
          "sampling": {
            "type": "number"
          },
          "batch_max": {
            "type": "integer"
          },
          "batch_wait_ms": {
            "type": "integer"
          },
          "ip_hash": {
            "type": "boolean"
          },
          "enabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EventDestinationInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "https",
              "ga4",
              "s3",
              "kafka"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "https: the endpoint; s3: the S3 endpoint https://host[:port]"
          },
          "config": {
            "$ref": "#/components/schemas/EventDestinationConfig"
          },
          "credentials": {
            "type": "object",
            "description": "Write-only, sealed at rest; omitted (or empty fields) on update = keep",
            "properties": {
              "access_key": {
                "type": "string",
                "description": "s3"
              },
              "secret_key": {
                "type": "string",
                "description": "s3"
              },
              "password": {
                "type": "string",
                "description": "kafka SASL password"
              }
            }
          },
          "format": {
            "type": "string",
            "enum": [
              "json",
              "ndjson"
            ]
          },
          "auth": {
            "type": "string",
            "enum": [
              "none",
              "bearer",
              "hmac"
            ]
          },
          "token": {
            "type": "string",
            "description": "bearer token (auth bearer), or the custom header's value (auth bearer + config.header_name); write-only, required when switching to bearer"
          },
          "api_secret": {
            "type": "string",
            "description": "GA4 Measurement Protocol API secret (kind ga4)"
          },
          "ga4_measurement_id": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "event types from the catalogue (groups player, ads, session — `session_summary`, one per closed session —, site, platform), or [\"*\"]"
          },
          "filters": {
            "$ref": "#/components/schemas/EventFilters"
          },
          "sampling": {
            "type": "number",
            "minimum": 0.001,
            "maximum": 1,
            "description": "share of sessions exported (session-consistent)"
          },
          "batch_max": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000
          },
          "batch_wait_ms": {
            "type": "integer",
            "minimum": 200,
            "maximum": 900000,
            "description": "at most 60000 except kind s3 (900000); s3 default 60000"
          },
          "ip_hash": {
            "type": "boolean"
          },
          "enabled": {
            "type": "boolean"
          },
          "rotate_secret": {
            "type": "boolean"
          }
        }
      },
      "EventDestinationConfig": {
        "type": "object",
        "description": "Non-secret settings of the s3 and kafka kinds (and the custom auth header of https + bearer)",
        "properties": {
          "bucket": {
            "type": "string",
            "description": "s3"
          },
          "prefix": {
            "type": "string",
            "description": "s3: object key prefix (no leading/trailing slash)"
          },
          "region": {
            "type": "string",
            "description": "s3: SigV4 region (default us-east-1)"
          },
          "brokers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "kafka: bootstrap host:port list (1–10)"
          },
          "topic": {
            "type": "string",
            "description": "kafka"
          },
          "tls": {
            "type": "boolean",
            "description": "kafka"
          },
          "ca_pem": {
            "type": "string",
            "description": "kafka: extra trusted CA certificate (PEM), with tls"
          },
          "sasl": {
            "type": "string",
            "enum": [
              "none",
              "plain",
              "scram-sha-256",
              "scram-sha-512"
            ],
            "description": "kafka (plain only with tls)"
          },
          "username": {
            "type": "string",
            "description": "kafka SASL user"
          },
          "header_name": {
            "type": "string",
            "maxLength": 64,
            "description": "https + auth bearer: send the token as the whole value of this header (e.g. X-API-Key, DD-API-KEY, or Authorization with \"Splunk <token>\") instead of Authorization: Bearer; empty = Bearer. Content-Type, Host, User-Agent, X-VS-* … are refused"
          }
        }
      },
      "EventDestinationState": {
        "type": "object",
        "description": "Export health (list responses)",
        "properties": {
          "last_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_status": {
            "type": "string",
            "enum": [
              "",
              "ok",
              "retrying",
              "dead"
            ]
          },
          "last_error": {
            "type": "string"
          },
          "dlq_pending": {
            "type": "integer",
            "description": "dead-letter batches not replayed"
          },
          "dlq_last_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "EventFilters": {
        "type": "object",
        "description": "Narrow statistics events (platform events are never filtered)",
        "properties": {
          "channels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "assets": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "EventDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "kind": {
            "type": "string",
            "enum": [
              "batch",
              "test",
              "replay"
            ]
          },
          "events": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "retrying",
              "dead"
            ]
          },
          "http_status": {
            "type": [
              "integer",
              "null"
            ]
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "attempt": {
            "type": "integer"
          },
          "error": {
            "type": "string"
          },
          "sample": {
            "type": "string"
          },
          "ref": {
            "type": "string",
            "description": "s3: bucket/object key; kafka: topic, partitions, first offset"
          }
        }
      },
      "EventDestinationStats": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "step_s": {
            "type": "integer",
            "description": "60 — one point per minute with traffic"
          },
          "points": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time",
                  "description": "start of the minute (UTC)"
                },
                "sent": {
                  "type": "integer",
                  "description": "events delivered"
                },
                "batches_ok": {
                  "type": "integer"
                },
                "failed": {
                  "type": "integer",
                  "description": "failed delivery attempts"
                },
                "dead": {
                  "type": "integer",
                  "description": "batches moved to the dead-letter queue"
                },
                "dead_events": {
                  "type": "integer"
                },
                "dropped": {
                  "type": "integer",
                  "description": "events dropped because the queue was full"
                },
                "backlog": {
                  "type": "integer",
                  "description": "queued undelivered events (maximum of the 15 s samples)"
                },
                "lag_ms_max": {
                  "type": "integer"
                },
                "lag_ms_avg": {
                  "type": "integer"
                }
              }
            }
          },
          "summary": {
            "type": "object",
            "properties": {
              "sent_per_min": {
                "type": "number",
                "description": "mean over the last 5 finished minutes"
              },
              "sent": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "dead": {
                "type": "integer"
              },
              "dead_events": {
                "type": "integer"
              },
              "dropped": {
                "type": "integer"
              },
              "lag_ms_avg": {
                "type": "integer",
                "description": "the newest minute with deliveries"
              },
              "lag_ms_max": {
                "type": "integer"
              },
              "backlog": {
                "type": "integer"
              },
              "last_error": {
                "type": "string"
              },
              "last_error_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        }
      },
      "ExportEvent": {
        "type": "object",
        "description": "One exported event (`schema: vs.event.v1`), as delivered in batches (JSON array / NDJSON / S3 object lines /\none Kafka record each). The viewer IP is never exported; `viewer_id` is the per-tenant hash and absent when\nconsent was denied. At-least-once: de-duplicate on `id`.",
        "properties": {
          "schema": {
            "type": "string",
            "enum": [
              "vs.event.v1"
            ]
          },
          "id": {
            "type": "string",
            "description": "uuid; for session_summary derived from tenant + session id (stable across re-emits)"
          },
          "type": {
            "type": "string",
            "description": "a catalogue event: player, ads, site, session (session_summary) or platform"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "event time (session_summary: the session's last event)"
          },
          "tenant": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          },
          "viewer_id": {
            "type": "string",
            "description": "hashed per tenant and year; absent without consent"
          },
          "consent": {
            "type": "string"
          },
          "content": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "live",
                  "vod",
                  "clip"
                ]
              },
              "channel": {
                "type": "string"
              },
              "asset": {
                "type": "string"
              },
              "clip": {
                "type": "string"
              },
              "live": {
                "type": "boolean"
              },
              "duration_ms": {
                "type": "integer"
              },
              "programme": {
                "type": "string",
                "description": "session_summary: EPG programme id the play is attributed to"
              }
            }
          },
          "player": {
            "type": "object",
            "additionalProperties": true
          },
          "device": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string"
              },
              "os": {
                "type": "string"
              },
              "browser": {
                "type": "string"
              }
            }
          },
          "geo": {
            "type": "object",
            "description": "coarse only",
            "properties": {
              "country": {
                "type": "string"
              },
              "asn": {
                "type": "integer"
              }
            }
          },
          "page": {
            "type": "object",
            "properties": {
              "host": {
                "type": "string"
              },
              "referrer_host": {
                "type": "string"
              }
            }
          },
          "metrics": {
            "type": "object",
            "additionalProperties": true
          },
          "error": {
            "type": "object",
            "additionalProperties": true
          },
          "ad": {
            "type": "object",
            "additionalProperties": true
          },
          "site": {
            "type": "object",
            "additionalProperties": true
          },
          "attrs": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "ip_hash": {
            "type": "string",
            "description": "only with ip_hash on the destination and consent"
          },
          "session": {
            "$ref": "#/components/schemas/ExportSessionSummary"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "platform events — the webhook payload"
          }
        }
      },
      "ExportSessionSummary": {
        "type": "object",
        "description": "The `session` block of a `session_summary` event — one per closed session, emitted by the sessioniser when the\nplayer ended the session (`session_end`/`ended`, after a 2-minute grace for late beacons) or after 15 minutes\nwithout events. A session that resumes after an idle close is emitted again later with the same event `id`\nand updated totals (keep the last one).",
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "closed_by": {
            "type": "string",
            "enum": [
              "end",
              "idle"
            ]
          },
          "played": {
            "type": "boolean",
            "description": "a first frame was shown"
          },
          "watch_time_ms": {
            "type": "integer",
            "description": "visible playback time"
          },
          "startup_ms": {
            "type": "integer",
            "description": "time to first frame; 0 = not measured"
          },
          "rebuffer_count": {
            "type": "integer"
          },
          "rebuffer_ms": {
            "type": "integer"
          },
          "error_count": {
            "type": "integer"
          },
          "fatal_error": {
            "type": "boolean"
          },
          "max_bitrate_kbps": {
            "type": "integer"
          },
          "avg_bitrate_kbps": {
            "type": "integer",
            "description": "mean declared bitrate over visible heartbeats"
          },
          "level_switches": {
            "type": "integer"
          },
          "completed": {
            "type": "boolean"
          },
          "completion_quartile": {
            "type": [
              "integer",
              "null"
            ],
            "enum": [
              0,
              25,
              50,
              75,
              100,
              null
            ],
            "description": "furthest quartile reached (on-demand plays with a known duration; null for live)"
          },
          "max_position_ms": {
            "type": "integer"
          },
          "duration_ms": {
            "type": "integer"
          },
          "ads": {
            "type": "object",
            "properties": {
              "requested": {
                "type": "integer"
              },
              "impressions": {
                "type": "integer"
              },
              "completed": {
                "type": "integer"
              },
              "skipped": {
                "type": "integer"
              },
              "clicked": {
                "type": "integer"
              },
              "errors": {
                "type": "integer"
              }
            }
          }
        }
      },
      "EventDLQBatch": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "events": {
            "type": "integer"
          },
          "last_error": {
            "type": "string"
          },
          "replay_requested_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "replayed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "EventInboxItem": {
        "type": "object",
        "description": "One request the tenant's test receiver accepted",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "content_type": {
            "type": "string"
          },
          "destination": {
            "type": "string",
            "description": "`X-VS-Destination`"
          },
          "batch": {
            "type": "string",
            "description": "`X-VS-Batch`"
          },
          "event_count": {
            "type": "string",
            "description": "`X-VS-Event-Count`"
          },
          "timestamp": {
            "type": "string",
            "description": "`X-VS-Timestamp`"
          },
          "signature": {
            "type": "string",
            "description": "`X-VS-Signature`"
          },
          "authorization": {
            "type": "string",
            "description": "Masked bearer token (`bearer …1234`) or empty"
          },
          "bytes": {
            "type": "integer",
            "description": "Body size"
          },
          "body": {
            "type": "string",
            "description": "The body (first 64 KB)"
          },
          "verified": {
            "type": "string",
            "enum": [
              "valid",
              "invalid",
              "unsigned"
            ],
            "description": "HMAC check against the tenant's hmac destinations"
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "required": [
          "id",
          "customer_id",
          "url",
          "events",
          "enabled",
          "created_at",
          "secret_hint"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Subscribed event types, or [\"*\"]"
          },
          "enabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "secret_hint": {
            "type": "string",
            "description": "First 10 characters of the signing secret followed by `…`"
          },
          "secret": {
            "type": "string",
            "description": "Signing secret (`whsec_` + 64 hex) — only in the create response"
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "required": [
          "id",
          "endpoint_id",
          "event_type",
          "event_id",
          "payload",
          "status",
          "attempts",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Delivery id (sent as `X-VS-Delivery`)"
          },
          "endpoint_id": {
            "type": "string",
            "format": "uuid"
          },
          "event_type": {
            "type": "string"
          },
          "event_id": {
            "type": "string",
            "format": "uuid",
            "description": "The envelope `id` (the same on every attempt and redelivery)"
          },
          "payload": {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed",
              "dead"
            ],
            "description": "pending = queued; failed = the last attempt failed and another is scheduled; dead = no further attempt"
          },
          "attempts": {
            "type": "integer",
            "description": "Attempts made so far"
          },
          "next_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_status_code": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP status of the last attempt (null when no response)"
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ],
            "description": "`HTTP <status>`, the transport error, or `endpoint disabled`"
          },
          "response_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "Body of every outbound webhook `POST` (`Content-Type: application/json`). Headers: `X-VS-Event` (= `type`),\n`X-VS-Delivery` (delivery id), `X-VS-Timestamp` (unix seconds), `X-VS-Signature`\n(`sha256=<hex HMAC-SHA256(secret, X-VS-Timestamp + \".\" + raw body)>`), `User-Agent: ViewStream-Webhooks/1`.\nVerify the signature over the raw bytes and reject stale timestamps; de-duplicate on `id`.",
        "required": [
          "id",
          "type",
          "at",
          "customer_id",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Event id"
          },
          "type": {
            "type": "string",
            "description": "Event type, e.g. asset.ready, clip.ready, channel.feed_changed, alert.firing, ping"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "When the event happened"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid",
            "description": "The tenant"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The event payload, e.g. asset.ready / asset.published: {asset_id, external_id, title, duration_ms, ladder, playback: {hls, dash, poster, sprite, thumbs_vtt, download}, published}; asset.failed: {asset_id, external_id, error, job_id, job_type}; ping: {endpoint_id, message}"
          }
        },
        "example": {
          "id": "01929a77-0c2e-7f10-9a2b-3c4d5e6f7a8b",
          "type": "asset.ready",
          "at": "2026-10-05T18:02:11Z",
          "customer_id": "01927e11-2a3b-7c4d-8e5f-60718293a4b5",
          "data": {
            "asset_id": "01929a51-7d3e-7a10-b2c3-d4e5f6a7b8c9",
            "external_id": "tv10-2026-1005-07",
            "title": "מהדורה מרכזית",
            "duration_ms": 1745320,
            "ladder": "default",
            "playback": {
              "hls": "https://cdn.tv10poc.vustream.net/v/01929a51-7d3e-7a10-b2c3-d4e5f6a7b8c9/master.m3u8"
            },
            "published": false
          }
        }
      },
      "InboundHook": {
        "type": "object",
        "required": [
          "id",
          "customer_id",
          "kind",
          "mapping",
          "enabled",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "cms_publish",
              "custom"
            ]
          },
          "mapping": {
            "$ref": "#/components/schemas/InboundHookMapping"
          },
          "enabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_received_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Last call with a valid signature"
          }
        }
      },
      "InboundHookMapping": {
        "type": "object",
        "description": "Where to find asset references in the CMS payload. Each value is a JSONPath-lite expression — `$.a.b`,\n`$.items[*].id`, `$.list[0].url` — whose string/number (or array of them) values are taken. At least one is\nrequired.",
        "additionalProperties": false,
        "properties": {
          "asset_external_ids": {
            "type": "string",
            "description": "Path to the assets' `external_id`s"
          },
          "asset_ids": {
            "type": "string",
            "description": "Path to ViewStream asset ids"
          },
          "urls": {
            "type": "string",
            "description": "Path to URLs on the tenant's CDN hostname (pre-warmed as given)"
          }
        }
      },
      "InboundHookExtracted": {
        "type": "object",
        "description": "What the hook's mapping found in the payload",
        "properties": {
          "asset_external_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "asset_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "urls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "SitesLocalized": {
        "description": "Localised text: a plain string, or a map of locale (he, en, ar, ru) to text",
        "oneOf": [
          {
            "type": "string"
          },
          {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        ]
      },
      "SeriesEPGMatch": {
        "type": [
          "object",
          "null"
        ],
        "description": "EPG matching rule: programmes of `channel` whose title matches any of `titles` or whose EPG external id is in `external_ids` become the show's catch-up episodes. Needs titles or external_ids; null = no matching.",
        "properties": {
          "channel": {
            "type": "string",
            "description": "Channel slug; omitted = any channel of the tenant"
          },
          "titles": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "description": "Case-insensitive Postgres regular expressions"
          },
          "external_ids": {
            "type": "array",
            "maxItems": 500,
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": false
      },
      "SeriesInput": {
        "type": "object",
        "description": "A show (series). On create `title` is required; on PATCH only the fields sent change.",
        "additionalProperties": false,
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 300
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "i18n": {
            "type": "object",
            "additionalProperties": true,
            "description": "Translations, e.g. {en: {title, description}}"
          },
          "kind": {
            "type": "string",
            "enum": [
              "show",
              "podcast",
              "collection"
            ],
            "default": "show"
          },
          "slug": {
            "type": "string",
            "maxLength": 120,
            "description": "URL slug; no / ? # % or spaces; default: the title slugified (Hebrew letters kept)"
          },
          "epg_match": {
            "$ref": "#/components/schemas/SeriesEPGMatch"
          },
          "images": {
            "type": "object",
            "additionalProperties": true,
            "description": "Image keys by role (poster, hero, …)"
          },
          "seo": {
            "type": "object",
            "additionalProperties": true,
            "description": "SEO overrides (title, description, …)"
          },
          "collection_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Library section; the nil UUID clears it"
          },
          "sort": {
            "type": "integer",
            "description": "Order in lists (ascending)"
          }
        }
      },
      "Series": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "i18n": {
            "type": "object",
            "additionalProperties": true
          },
          "kind": {
            "type": "string",
            "enum": [
              "show",
              "podcast",
              "collection"
            ]
          },
          "slug": {
            "type": "string"
          },
          "epg_match": {
            "$ref": "#/components/schemas/SeriesEPGMatch"
          },
          "images": {
            "type": "object",
            "additionalProperties": true
          },
          "seo": {
            "type": "object",
            "additionalProperties": true
          },
          "collection_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "sort": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "vod_count": {
            "type": "integer",
            "description": "VOD episodes (list only; 0 elsewhere)"
          },
          "catchup_count": {
            "type": "integer",
            "description": "Matched EPG programmes (list only; 0 elsewhere)"
          }
        }
      },
      "SeriesEpisodes": {
        "type": "object",
        "properties": {
          "vod": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "asset_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "season": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "episode": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "position": {
                  "type": "integer",
                  "description": "0-based order as saved"
                },
                "duration_s": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "status": {
                  "type": "string",
                  "description": "Asset status"
                }
              }
            }
          },
          "catchup": {
            "type": "array",
            "maxItems": 500,
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "Programme id"
                },
                "channel": {
                  "type": "string",
                  "description": "Channel slug"
                },
                "start_at": {
                  "type": "string",
                  "format": "date-time"
                },
                "end_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          }
        }
      },
      "PersonInput": {
        "type": "object",
        "description": "A person. On create `name` is required; on PATCH only the fields sent change.",
        "additionalProperties": false,
        "properties": {
          "slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Unique in the tenant; used in /person/:slug"
          },
          "name": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "role": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "bio": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "image_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage key of the portrait"
          },
          "links": {
            "type": "object",
            "additionalProperties": true,
            "description": "Free-form links (social profiles, website)"
          }
        }
      },
      "Person": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "role": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "bio": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "image_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "links": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "SiteSettings": {
        "type": "object",
        "description": "Site settings document (free-form JSON; these keys are used and validated). PATCH replaces it whole except custom_domains.",
        "additionalProperties": true,
        "properties": {
          "seo": {
            "type": "object",
            "additionalProperties": true
          },
          "scripts": {
            "type": "array",
            "description": "Allow-listed external scripts, loaded by consent category",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"
                },
                "src": {
                  "type": "string"
                },
                "placement": {
                  "type": "string",
                  "enum": [
                    "head",
                    "body"
                  ]
                },
                "category": {
                  "type": "string",
                  "enum": [
                    "necessary",
                    "analytics",
                    "ads"
                  ]
                },
                "async": {
                  "type": "boolean"
                }
              }
            }
          },
          "consent": {
            "type": "object",
            "additionalProperties": true,
            "description": "Cookie consent: policy_url (path or https URL), texts {he|en|ar|ru: {title, body, accept, reject, customize, save, analytics, ads} ≤ 800 chars}"
          },
          "analytics": {
            "type": "object",
            "properties": {
              "ga4_id": {
                "type": "string",
                "pattern": "^G-[A-Z0-9]{4,14}$"
              },
              "gtm_id": {
                "type": "string",
                "pattern": "^GTM-[A-Z0-9]{4,12}$"
              }
            }
          },
          "ads": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "gam_network": {
                "type": "string",
                "description": "Google Ad Manager network code (digits; required when enabled)"
              }
            }
          },
          "accessibility": {
            "type": "object",
            "additionalProperties": true
          },
          "dictionary": {
            "type": "object",
            "additionalProperties": true,
            "description": "UI string overrides per locale"
          },
          "player": {
            "type": "object",
            "properties": {
              "config": {
                "type": "string",
                "description": "Player configuration the site's players load"
              }
            }
          },
          "epg": {
            "type": "object",
            "properties": {
              "days_back": {
                "type": "integer",
                "maximum": 30,
                "default": 7
              },
              "days_forward": {
                "type": "integer",
                "maximum": 30,
                "default": 7
              },
              "channels": {
                "type": "object",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "publish": {
                      "type": "boolean",
                      "default": true
                    }
                  }
                },
                "description": "Per channel slug: publish the guide on this site"
              }
            }
          },
          "video_ads": {
            "type": "object",
            "additionalProperties": true,
            "description": "mode preset|off|custom; custom needs an https VAST tag and positions pre|mid|post"
          },
          "headless": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "origins": {
                "type": "array",
                "maxItems": 20,
                "items": {
                  "type": "string"
                },
                "description": "https://… origins, http://localhost[:port], or *"
              }
            }
          },
          "ai_crawlers": {
            "type": "string",
            "enum": [
              "allow",
              "block"
            ]
          },
          "custom_domains": {
            "type": "array",
            "readOnly": true,
            "items": {
              "$ref": "#/components/schemas/SiteCustomDomain"
            },
            "description": "Managed by /v1/sites/{id}/domains"
          }
        }
      },
      "SiteInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "slug": {
            "type": "string",
            "maxLength": 63,
            "pattern": "^[a-z0-9-]+$",
            "description": "Unique across ViewStream; default on create: the tenant slug"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "hostnames": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Replaces the list; default on create: [<slug>.viewstream.co.il]"
          },
          "locales": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "he",
                "en",
                "ar",
                "ru"
              ]
            },
            "description": "Default [he]"
          },
          "default_locale": {
            "type": "string",
            "enum": [
              "he",
              "en",
              "ar",
              "ru"
            ],
            "description": "Must be one of locales; default he"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "live",
              "suspended"
            ],
            "description": "Default draft; suspended answers 404 to the public"
          },
          "settings": {
            "$ref": "#/components/schemas/SiteSettings"
          }
        }
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "hostnames": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "First = canonical host (an active custom domain comes first)"
          },
          "locales": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "default_locale": {
            "type": "string"
          },
          "theme_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "live",
              "suspended"
            ]
          },
          "settings": {
            "$ref": "#/components/schemas/SiteSettings"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string",
            "description": "https:// + hostnames[0]; absent without hostnames"
          }
        }
      },
      "SiteCustomDomain": {
        "type": "object",
        "properties": {
          "hostname": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "needs_cloudflare_saas",
              "pending",
              "active",
              "error"
            ]
          },
          "cf_id": {
            "type": "string",
            "description": "Cloudflare custom hostname id (once provisioned)"
          },
          "cname_target": {
            "type": "string",
            "description": "What to CNAME the hostname to"
          },
          "txt": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "value": {
                  "type": "string"
                }
              }
            },
            "description": "Ownership and certificate validation records to add at your DNS provider"
          },
          "message": {
            "type": "string",
            "description": "Why the domain is not active (error or missing setup)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SiteDomains": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteCustomDomain"
            }
          },
          "cname_target": {
            "type": "string",
            "example": "sites-origin.viewstream.co.il"
          },
          "saas_configured": {
            "type": "boolean",
            "description": "Cloudflare for SaaS is set up on this deployment"
          },
          "note": {
            "type": "string",
            "description": "What an operator must enable when saas_configured is false"
          }
        }
      },
      "SiteBlock": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string"
          },
          "props": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Allowed data-source kinds (\"\" = the block may have no source)"
          },
          "list": {
            "type": "boolean"
          },
          "island": {
            "type": "boolean",
            "description": "Hydrates in the browser"
          },
          "platforms": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "web",
                "app",
                "tv"
              ]
            }
          },
          "admin_only": {
            "type": "boolean"
          }
        }
      },
      "SiteBuilderItem": {
        "type": "object",
        "description": "One resolved tile (the Delivery API item shape)",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "channel",
              "programme",
              "asset",
              "clip",
              "series",
              "person",
              "page",
              "banner",
              "collection"
            ]
          },
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "href": {
            "type": "string"
          },
          "title": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "subtitle": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "description": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "images": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "duration_s": {
            "type": "integer"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "badge": {
            "type": "string",
            "enum": [
              "live",
              "catchup",
              "new"
            ]
          },
          "preview_url": {
            "type": "string"
          },
          "rank": {
            "type": "integer"
          },
          "season": {
            "type": "integer"
          },
          "episode": {
            "type": "integer"
          },
          "audio_url": {
            "type": "string"
          },
          "summary_short": {
            "type": "string",
            "description": "programme and asset items — the short content summary"
          },
          "presenters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SitesDeliveryPresenter"
            }
          }
        }
      },
      "SiteRouteResult": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "template",
              "redirect",
              "not_found"
            ]
          },
          "page_id": {
            "type": "string",
            "format": "uuid",
            "description": "The page or template page that renders the path"
          },
          "template_for": {
            "type": "string"
          },
          "entity": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              }
            }
          },
          "redirect": {
            "type": "object",
            "properties": {
              "to": {
                "type": "string"
              },
              "code": {
                "type": "integer"
              }
            }
          }
        }
      },
      "SiteTheme": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "tokens": {
            "type": "object",
            "additionalProperties": true,
            "description": "Draft design tokens"
          },
          "custom_css": {
            "type": [
              "string",
              "null"
            ],
            "description": "Draft CSS"
          },
          "published_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "published_tokens": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SiteContrastRow": {
        "type": "object",
        "properties": {
          "pair": {
            "type": "string",
            "example": "fg / bg"
          },
          "ratio": {
            "type": "number",
            "description": "Contrast ratio (2 decimals)"
          },
          "ok": {
            "type": "boolean",
            "description": "≥ 4.5:1"
          }
        }
      },
      "SiteMenuItem": {
        "type": "object",
        "required": [
          "kind"
        ],
        "additionalProperties": true,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "entity",
              "url",
              "separator",
              "accessibility",
              "search",
              "logo"
            ]
          },
          "label": {
            "$ref": "#/components/schemas/SitesLocalized"
          },
          "href": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          }
        }
      },
      "SiteMenu": {
        "type": "object",
        "properties": {
          "slot": {
            "type": "string",
            "enum": [
              "header_web",
              "header_mobile",
              "footer",
              "legal"
            ]
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteMenuItem"
            }
          },
          "published_items": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/SiteMenuItem"
            }
          },
          "published_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "SiteRoute": {
        "type": "object",
        "required": [
          "pattern",
          "target"
        ],
        "properties": {
          "pattern": {
            "type": "string",
            "maxLength": 200,
            "description": "Path pattern with :param segments, e.g. /tochniot/:slug"
          },
          "target": {
            "type": "string",
            "description": "A page id, or a template kind: show, asset, clip, programme, channel, person, collection, podcast, search, epg, not_found"
          },
          "priority": {
            "type": "integer",
            "default": 0,
            "description": "Higher first"
          }
        }
      },
      "SiteRedirect": {
        "type": "object",
        "required": [
          "from_path",
          "to_path"
        ],
        "properties": {
          "from_path": {
            "type": "string",
            "maxLength": 200
          },
          "to_path": {
            "type": "string",
            "description": "Site path or absolute URL"
          },
          "code": {
            "type": "integer",
            "enum": [
              301,
              302,
              307,
              308
            ],
            "default": 301
          },
          "source": {
            "type": "string",
            "enum": [
              "manual",
              "import"
            ],
            "description": "Default manual"
          },
          "hits": {
            "type": "integer",
            "readOnly": true
          }
        }
      },
      "SiteRedirectIssue": {
        "type": "object",
        "properties": {
          "line": {
            "type": "integer",
            "description": "CSV line (parse errors)"
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warning"
            ],
            "description": "error = row skipped; warning = imported"
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "SiteRedirectImport": {
        "type": "object",
        "properties": {
          "dry_run": {
            "type": "boolean"
          },
          "rows": {
            "type": "integer",
            "description": "Rows read (including unreadable ones)"
          },
          "valid": {
            "type": "integer"
          },
          "added": {
            "type": "integer",
            "description": "0 on a dry run"
          },
          "updated": {
            "type": "integer",
            "description": "0 on a dry run"
          },
          "skipped": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "maxItems": 500,
            "items": {
              "$ref": "#/components/schemas/SiteRedirect"
            },
            "description": "Preview of the valid rows (first 500)"
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteRedirectIssue"
            }
          }
        }
      },
      "SiteRedirectChain": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string"
          },
          "hops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteRedirect"
            }
          },
          "final": {
            "type": "string",
            "description": "Where the chain ends (path or external URL)"
          },
          "status": {
            "type": "string",
            "enum": [
              "no_redirect",
              "ok",
              "chain",
              "loop",
              "too_long"
            ]
          }
        }
      },
      "Problem": {
        "type": "object",
        "required": [
          "type",
          "title",
          "status"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "detail"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "TenantSummary": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "name",
          "mode",
          "cdn_hostname",
          "role"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "enum": [
              "platform",
              "cdn",
              "ai",
              "drm"
            ]
          },
          "cdn_hostname": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "description": "Membership role for sessions; `api_key` for keys",
            "enum": [
              "viewer",
              "editor",
              "publisher",
              "engineer",
              "admin",
              "owner",
              "api_key"
            ]
          }
        }
      },
      "User": {
        "type": "object",
        "required": [
          "id",
          "email",
          "name",
          "locale",
          "tz",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "default": "he"
          },
          "tz": {
            "type": "string",
            "default": "Asia/Jerusalem"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_login_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "auth",
          "tenants",
          "current_tenant",
          "scopes"
        ],
        "properties": {
          "auth": {
            "type": "string",
            "enum": [
              "session",
              "api_key"
            ]
          },
          "user": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/User"
              },
              {
                "type": "null"
              }
            ],
            "description": "null for an API key"
          },
          "tenants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TenantSummary"
            }
          },
          "current_tenant": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/TenantSummary"
              },
              {
                "type": "null"
              }
            ],
            "description": "null for a session that has not chosen a tenant"
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            }
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "required": [
          "id",
          "customer_id",
          "key_prefix",
          "scopes",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "key_prefix": {
            "type": "string",
            "minLength": 8,
            "maxLength": 8,
            "description": "The 8 characters after `vs_` (identifies the key; not secret)"
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            }
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ApiKeyCreate": {
        "type": "object",
        "required": [
          "scopes"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120,
            "description": "Label (who uses the key)"
          },
          "scopes": {
            "type": "array",
            "minItems": 1,
            "description": "Known scopes the caller itself holds",
            "items": {
              "$ref": "#/components/schemas/Scope"
            }
          }
        }
      },
      "ApiKeyCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "required": [
              "key"
            ],
            "properties": {
              "key": {
                "type": "string",
                "pattern": "^vs_[0-9A-Za-z]{8}_[0-9A-Za-z]{32}$",
                "description": "Plaintext key `vs_<prefix>_<secret>`, shown once"
              }
            }
          }
        ]
      },
      "Scope": {
        "type": "string",
        "description": "Addendum A3. Roles map to scopes cumulatively — Viewer, Editor, Publisher, Engineer, Admin, Owner.",
        "enum": [
          "assets:read",
          "assets:write",
          "assets:publish",
          "clips:read",
          "clips:write",
          "clips:publish",
          "channels:read",
          "channels:write",
          "channels:operate",
          "uploads",
          "prewarm",
          "delivery:write",
          "webhooks:manage",
          "storage:manage",
          "keys:manage",
          "stats:read",
          "stats:pii",
          "events:read",
          "team:manage",
          "notifications:manage",
          "billing:read",
          "tenant:settings",
          "assets:purge",
          "sites:read",
          "sites:write",
          "sites:publish",
          "sites:admin",
          "playback:sign",
          "security:manage",
          "epg:write",
          "epg:publish",
          "support:read",
          "support:write",
          "ai:read",
          "ai:write",
          "ai:publish"
        ]
      },
      "AssetPage": {
        "type": "object",
        "required": [
          "items",
          "next_cursor"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssetSummary"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "AssetSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "registered",
              "probing",
              "queued",
              "encoding",
              "packaging",
              "ready",
              "failed",
              "deleted"
            ]
          },
          "ladder": {
            "type": "string"
          },
          "duration_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ready_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "deleted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the asset went to the trash"
          },
          "match": {
            "$ref": "#/components/schemas/SearchMatch"
          }
        }
      },
      "SearchMatch": {
        "type": "object",
        "description": "Search hint (GET /v1/assets?q=, GET /v1/channels/{id}/catchup/search): the field that contained the query and, for description / summary, the words around the hit (\"…\" where cut); for presenters / topics, the matching entry. Absent when the hint could not be located (the result still matched).",
        "required": [
          "field"
        ],
        "properties": {
          "field": {
            "type": "string",
            "enum": [
              "title",
              "external_id",
              "id",
              "description",
              "presenters",
              "topics",
              "summary",
              "transcript"
            ],
            "description": "transcript: found in what was said — the snippet is the subtitle line, the item carries moments"
          },
          "snippet": {
            "type": "string"
          }
        }
      },
      "AssetCreate": {
        "type": "object",
        "required": [
          "source"
        ],
        "additionalProperties": false,
        "properties": {
          "external_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9._:/-]{1,200}$",
            "description": "CMS id, unique per tenant (409 when taken)"
          },
          "title": {
            "type": "string",
            "maxLength": 500
          },
          "source": {
            "type": "object",
            "required": [
              "kind"
            ],
            "additionalProperties": false,
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "s3",
                  "url"
                ],
                "description": "`upload` is refused here — use POST /v1/uploads"
              },
              "key": {
                "type": "string",
                "description": "s3: key in the ingest bucket under `<tenant prefix>/in/`, no `..`"
              },
              "url": {
                "type": "string",
                "format": "uri",
                "description": "url: http(s) source the worker downloads; no credentials in the URL, port 80/443/8443 only, the host must resolve to a public address (SSRF guard)"
              }
            }
          },
          "ladder": {
            "type": "string",
            "description": "Ladder profile name (e.g. news-1080p, news-720p, hevc-1080p); default the tenant's default_ladder"
          },
          "publish": {
            "type": "string",
            "enum": [
              "auto",
              "manual"
            ],
            "default": "auto",
            "description": "manual = stays unpublished when ready until PATCH {publish: true}"
          },
          "metadata": {
            "type": "object",
            "description": "Free-form JSON object, max 16 KB. `metadata.ads` is checked: `{disabled?: boolean, cues?: [seconds > 0 and ≤ 86400, at most 50]}`. Keys starting with `_` are reserved for the platform (e.g. `_publish`)."
          }
        }
      },
      "LibraryImportFilters": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "description": "published on or after (RFC 3339 or YYYY-MM-DD)"
          },
          "to": {
            "type": "string",
            "description": "published on or before (RFC 3339 or YYYY-MM-DD, whole day)"
          },
          "path_prefix": {
            "type": "string",
            "maxLength": 512
          },
          "source_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "mp4",
                "hls"
              ]
            }
          },
          "include_duplicates": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "LibraryImport": {
        "type": "object",
        "description": "A library import from a sitemap. Status: discovering → discovered (or failed) → importing (after start) → done; cancelled after cancel.",
        "required": [
          "id",
          "sitemap_url",
          "status",
          "options",
          "import_options",
          "resync",
          "next_sync_at",
          "last_sync_at",
          "error",
          "created_by",
          "created_at",
          "updated_at",
          "finished_at",
          "stats",
          "counts"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "sitemap_url": {
            "type": "string",
            "format": "uri"
          },
          "status": {
            "type": "string",
            "enum": [
              "discovering",
              "discovered",
              "importing",
              "done",
              "cancelled",
              "failed"
            ]
          },
          "options": {
            "type": "object",
            "required": [
              "extract_pages",
              "max_items"
            ],
            "properties": {
              "extract_pages": {
                "type": "boolean"
              },
              "max_items": {
                "type": "integer"
              }
            }
          },
          "import_options": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "publish": {
                "type": "string",
                "enum": [
                  "auto",
                  "manual"
                ]
              },
              "subtitles": {
                "type": "boolean"
              },
              "filters": {
                "$ref": "#/components/schemas/LibraryImportFilters"
              }
            }
          },
          "resync": {
            "type": "boolean",
            "description": "Daily re-sync is on (PATCH)"
          },
          "next_sync_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the next re-sync runs (null when re-sync is off)"
          },
          "last_sync_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the last re-sync finished"
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why discovery failed (status failed)"
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who created it: `user:<e-mail>` or `key:<key prefix>`"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "stats": {
            "type": "object",
            "required": [
              "sitemaps",
              "pages",
              "requests",
              "robots_blocked",
              "truncated",
              "errors"
            ],
            "properties": {
              "sitemaps": {
                "type": "integer",
                "description": "sitemap files read"
              },
              "pages": {
                "type": "integer",
                "description": "pages opened for extraction"
              },
              "requests": {
                "type": "integer"
              },
              "robots_blocked": {
                "type": "integer"
              },
              "truncated": {
                "type": "boolean",
                "description": "max_items reached — more videos may exist"
              },
              "errors": {
                "type": "array",
                "maxItems": 20,
                "items": {
                  "type": "object",
                  "required": [
                    "url",
                    "error"
                  ],
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "counts": {
            "type": "object",
            "description": "items by source type, duplicates and import status",
            "required": [
              "total",
              "mp4",
              "hls",
              "unsupported",
              "duplicates",
              "discovered",
              "queued",
              "fetching",
              "processing",
              "imported",
              "failed",
              "skipped",
              "cancelled"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "mp4": {
                "type": "integer"
              },
              "hls": {
                "type": "integer"
              },
              "unsupported": {
                "type": "integer"
              },
              "duplicates": {
                "type": "integer"
              },
              "discovered": {
                "type": "integer"
              },
              "queued": {
                "type": "integer"
              },
              "fetching": {
                "type": "integer"
              },
              "processing": {
                "type": "integer"
              },
              "imported": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "skipped": {
                "type": "integer"
              },
              "cancelled": {
                "type": "integer"
              }
            }
          }
        }
      },
      "LibraryImportItem": {
        "type": "object",
        "required": [
          "id",
          "source_url",
          "source_type",
          "via",
          "status",
          "duplicate",
          "tags",
          "attempts",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "source_url": {
            "type": "string",
            "description": "the media URL (or the page URL when no video was found)"
          },
          "page_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "thumbnail_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "duration_s": {
            "type": [
              "integer",
              "null"
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "source_type": {
            "type": "string",
            "enum": [
              "mp4",
              "hls",
              "unsupported"
            ]
          },
          "via": {
            "type": "string",
            "enum": [
              "video_sitemap",
              "og_video",
              "json_ld",
              "html",
              "direct",
              "page"
            ],
            "description": "where the media URL was found"
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "why an item is unsupported (e.g. embed player, DASH, no video found)"
          },
          "status": {
            "type": "string",
            "enum": [
              "discovered",
              "queued",
              "fetching",
              "processing",
              "imported",
              "failed",
              "skipped",
              "cancelled"
            ]
          },
          "duplicate": {
            "type": "boolean",
            "description": "the library already has a non-deleted asset from this source URL"
          },
          "asset_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "asset_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "attempts": {
            "type": "integer"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PosterState": {
        "type": "object",
        "description": "The poster of one programme, series, asset or clip. `current` is what the poster chain\nuses from ViewStream's side: the editor's choice when set (`mode: editor`), else the automatic best still\n(`mode: auto`), else nothing (`mode: none`). The automatic selection never changes the editor's choice.",
        "required": [
          "entity_type",
          "entity_id",
          "mode",
          "current",
          "auto",
          "editor",
          "candidates",
          "suggest"
        ],
        "properties": {
          "entity_type": {
            "type": "string",
            "enum": [
              "programme",
              "series",
              "asset",
              "clip"
            ]
          },
          "entity_id": {
            "type": "string",
            "format": "uuid"
          },
          "mode": {
            "type": "string",
            "enum": [
              "editor",
              "auto",
              "none"
            ]
          },
          "current": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PosterImage"
              },
              {
                "type": "null"
              }
            ]
          },
          "auto": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PosterImage"
              },
              {
                "type": "null"
              }
            ],
            "description": "The automatic choice (with its score); null before scoring"
          },
          "editor": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PosterImage"
              },
              {
                "type": "null"
              }
            ],
            "description": "The editor's choice (locked)",
            "null when automatic": null
          },
          "editor_by": {
            "type": "string",
            "description": "Who chose it (`user:<e-mail>` or `key:<key prefix>`); omitted without an editor's choice"
          },
          "editor_at": {
            "type": "string",
            "format": "date-time",
            "description": "Omitted without an editor's choice"
          },
          "candidates": {
            "type": "array",
            "maxItems": 36,
            "description": "Scored stills, best first (the regular top 12, then up to 8 extras per \"suggest more\"). For a series without\ncandidates of its own: the top 3 stills of each of its 4 latest scored broadcasts.",
            "items": {
              "$ref": "#/components/schemas/PosterImage"
            }
          },
          "scored_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the automatic choice was last updated; omitted before scoring"
          },
          "suggest": {
            "type": "object",
            "description": "The newest \"suggest more\" job: `none` (none, or it succeeded), `queued`, `running` or `failed`",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "none",
                  "queued",
                  "running",
                  "failed"
                ]
              },
              "job_id": {
                "type": "string",
                "format": "uuid",
                "description": "Omitted when `none`"
              }
            }
          }
        }
      },
      "PosterImage": {
        "type": "object",
        "description": "One poster image. The scoring fields are present on scored stills only (candidates and the automatic choice).",
        "required": [
          "path",
          "url"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Origin path: /rec/<tenant>/… (a recorder still) or /vod/<tenant>/… (an uploaded or copied image)"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The image on the tenant's CDN hostname"
          },
          "score": {
            "type": "number",
            "description": "Overall poster score 0–1 (faces with eyes open and mouth closed, sharpness, no graphics)"
          },
          "faces": {
            "type": "integer",
            "description": "Faces detected"
          },
          "eyes_open": {
            "type": "number",
            "description": "0–1"
          },
          "mouth_ok": {
            "type": "number",
            "description": "0–1 (mouth closed / natural)"
          },
          "more": {
            "type": "boolean",
            "description": "true = an extra appended by \"suggest more\" (omitted otherwise)"
          }
        }
      },
      "Trash": {
        "type": "object",
        "required": [
          "count",
          "bytes",
          "retention_days",
          "items",
          "truncated"
        ],
        "properties": {
          "count": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer",
            "format": "int64",
            "description": "What the trashed assets still hold in storage (masters, ingest source, leftover renditions)"
          },
          "retention_days": {
            "type": "integer",
            "description": "Days a trashed asset is kept before it is purged automatically"
          },
          "truncated": {
            "type": "boolean",
            "description": "More than 1000 trashed assets; only the newest 1000 are listed"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "title",
                "deleted_at",
                "purge_at",
                "bytes",
                "restorable",
                "purge_pending"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "deleted_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "purge_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "description": "deleted_at + retention_days: when the automatic purge removes it"
                },
                "bytes": {
                  "type": "integer",
                  "format": "int64",
                  "description": "Renditions left under vod/, the master and the ingest source"
                },
                "restorable": {
                  "type": "boolean",
                  "description": "The master still exists and no permanent delete is pending"
                },
                "purge_pending": {
                  "type": "boolean",
                  "description": "A permanent delete job is queued or running"
                }
              }
            }
          },
          "channels": {
            "type": "array",
            "description": "Channels in the trash; purged at purge_at, not by Empty trash",
            "items": {
              "type": "object",
              "required": [
                "id",
                "slug",
                "title",
                "deleted_at",
                "purge_at"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "slug": {
                  "type": "string"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "deleted_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "purge_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "DistributionCertificate": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "managed",
              "uploaded"
            ]
          },
          "note": {
            "type": "string"
          },
          "certificate": {
            "type": "object",
            "properties": {
              "distribution_id": {
                "type": "string",
                "format": "uuid"
              },
              "sans": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "issuer": {
                "type": "string"
              },
              "not_before": {
                "type": "string",
                "format": "date-time"
              },
              "not_after": {
                "type": "string",
                "format": "date-time"
              },
              "fingerprint_sha256": {
                "type": "string"
              },
              "uploaded_by": {
                "type": "string"
              },
              "uploaded_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "Asset": {
        "type": "object",
        "description": "An asset as every asset route returns it: the stored row plus published, renditions, playback and the last 50 jobs",
        "required": [
          "id",
          "customer_id",
          "status",
          "ladder",
          "created_at",
          "updated_at",
          "published",
          "renditions",
          "playback",
          "jobs"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "registered",
              "probing",
              "queued",
              "encoding",
              "packaging",
              "ready",
              "failed",
              "deleted"
            ]
          },
          "poster_url": {
            "type": "string",
            "format": "uri",
            "description": "List pages only, and only on a tenant whose playback policy requires tokens: the ready asset's poster, signed for that one file (≥ 24 h, not bound to an IP). Absent otherwise — the poster is then https://<cdn_hostname>/vod/<tenant>/<id>/poster.jpg"
          },
          "source": {
            "type": "object",
            "description": "Where the master came from",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "s3",
                  "url",
                  "upload"
                ]
              },
              "bucket": {
                "type": "string",
                "description": "s3 / upload: ingest"
              },
              "key": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "master_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "Key of the master copy in the masters bucket (set after the probe)"
          },
          "ladder": {
            "type": "string"
          },
          "probe": {
            "type": [
              "object",
              "null"
            ],
            "description": "ffprobe summary of the source (width, height, fps, duration_ms, video_codec, audio_codec, interlaced, …)"
          },
          "duration_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the asset failed (status failed)"
          },
          "metadata": {
            "type": "object",
            "description": "The customer's metadata; `_publish: manual` marks a manual-publish asset"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ready_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "deleted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the asset went to the trash"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "published": {
            "type": "boolean",
            "description": "published_at is set"
          },
          "renditions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rendition"
            }
          },
          "playback": {
            "type": [
              "object",
              "null"
            ],
            "description": "Only while ready (null otherwise); URLs on the tenant's CDN hostname",
            "properties": {
              "hls": {
                "type": "string",
                "format": "uri"
              },
              "dash": {
                "type": "string",
                "format": "uri"
              },
              "poster": {
                "type": "string",
                "format": "uri"
              },
              "sprite": {
                "type": "string",
                "format": "uri"
              },
              "thumbs_vtt": {
                "type": "string",
                "format": "uri"
              },
              "download": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "jobs": {
            "type": "array",
            "description": "The asset's last 50 jobs, newest first",
            "items": {
              "$ref": "#/components/schemas/JobSummary"
            }
          }
        }
      },
      "Rendition": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "video",
              "audio",
              "poster",
              "sprite",
              "thumbs_vtt",
              "download_mp4",
              "master_m3u8",
              "mpd"
            ]
          },
          "label": {
            "type": "string"
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "bitrate_kbps": {
            "type": "integer"
          },
          "codec": {
            "type": "string"
          },
          "s3_key": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer",
            "format": "int64"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "JobSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "attempts": {
            "type": "integer"
          },
          "error": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Job": {
        "type": "object",
        "description": "A processing job. `payload` and `result` are type-specific JSON (internal object keys, not a stable contract).",
        "required": [
          "id",
          "customer_id",
          "type",
          "priority",
          "status",
          "payload",
          "result",
          "error",
          "attempts",
          "max_attempts",
          "worker_id",
          "lease_until",
          "asset_id",
          "clip_id",
          "channel_id",
          "created_at",
          "dispatched_at",
          "started_at",
          "finished_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "type": {
            "$ref": "#/components/schemas/JobType"
          },
          "priority": {
            "type": "integer",
            "enum": [
              1,
              2,
              3
            ],
            "description": "1 fast, 2 standard, 3 bulk"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "dispatched",
              "running",
              "succeeded",
              "failed",
              "cancelled"
            ]
          },
          "payload": {
            "type": "object"
          },
          "result": {
            "type": [
              "object",
              "null"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Last failure (internal addresses shown as [internal] on GET /v1/jobs/{id})"
          },
          "attempts": {
            "type": "integer"
          },
          "max_attempts": {
            "type": "integer"
          },
          "worker_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "lease_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Lease while dispatched/running; earliest dispatch time while queued"
          },
          "asset_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "clip_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "channel_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "dispatched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "JobType": {
        "type": "string",
        "description": "Job types (internal/jobs AllTypes)",
        "enum": [
          "probe",
          "transcode",
          "package",
          "thumbs",
          "clip_finalize",
          "reencode",
          "prewarm",
          "copy_master",
          "purge",
          "stats_export",
          "viewer_delete",
          "preview",
          "podcast_audio",
          "subtitles",
          "subtitles_translate",
          "programme_bounds",
          "lipsync_sample",
          "lipsync_correct",
          "poster_select",
          "import_fetch",
          "image_upscale",
          "video_upscale",
          "image_ingest",
          "bounds_features",
          "bounds_train",
          "content_summary",
          "creative_transcode"
        ]
      },
      "JobEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "level": {
            "type": "string",
            "enum": [
              "info",
              "warn",
              "error"
            ]
          },
          "message": {
            "type": "string"
          },
          "data": {
            "type": "object"
          }
        }
      },
      "JobIgnoreOutcome": {
        "type": "object",
        "required": [
          "id",
          "ignored"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "ignored": {
            "type": "boolean",
            "description": "the mark after the call (false also when the call failed)"
          },
          "error": {
            "type": "string",
            "description": "bulk call only: no such job, or the job is not failed"
          }
        }
      },
      "JobRetryOutcome": {
        "type": "object",
        "required": [
          "id",
          "retried"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "dispatched",
              "running",
              "succeeded",
              "failed",
              "cancelled"
            ],
            "description": "the job's status after the call (absent when the job was not found)"
          },
          "retried": {
            "type": "boolean",
            "description": "true when this call re-queued the job"
          },
          "note": {
            "type": "string",
            "enum": [
              "already queued or running",
              "already succeeded",
              "already retried"
            ],
            "description": "why nothing was done (not an error)"
          },
          "error": {
            "type": "string",
            "description": "why the job cannot be retried (bulk call only; the single call answers 404/422 instead)"
          }
        }
      },
      "JobWithEvents": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Job"
          },
          {
            "type": "object",
            "properties": {
              "events": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/JobEvent"
                }
              }
            }
          }
        ]
      },
      "Upload": {
        "type": "object",
        "required": [
          "upload_id",
          "key",
          "part_size",
          "parts",
          "expires_at"
        ],
        "properties": {
          "upload_id": {
            "type": "string",
            "description": "Object-store multipart upload id; the `{id}` of the part and complete routes"
          },
          "key": {
            "type": "string",
            "description": "Ingest key `<tenant>/in/<id>/<filename>`; pass it to the part and complete routes"
          },
          "part_size": {
            "type": "integer",
            "description": "67108864 (64 MB); every part but the last has this size"
          },
          "parts": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "part_number",
                "url"
              ],
              "properties": {
                "part_number": {
                  "type": "integer"
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Presigned PUT URL on the internal S3 endpoint, valid until expires_at"
                }
              }
            }
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "1 hour after the start"
          },
          "upload_token": {
            "type": "string",
            "description": "With `browser_origin` only: the upload-only token for `PUT /v1/upload-parts/{n}?t=…` (hand it to the browser, never the key)"
          },
          "part_url": {
            "type": "string",
            "description": "With `browser_origin` only: the part URL template, `/v1/upload-parts/{n}?t=<token>` (relative to the API base)"
          },
          "upload_token_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "With `browser_origin` only: when the token stops working (with the upload)"
          }
        }
      },
      "Channel": {
        "type": "object",
        "description": "channels row + M3 columns + playback URL templates",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
            "description": "Unique per tenant; part of the playback paths. Cannot be changed."
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "ladder": {
            "type": "string",
            "description": "Ladder profile name the encoder renders (e.g. `news-1080p-hi`)"
          },
          "dvr_window_s": {
            "type": "integer",
            "minimum": 60,
            "maximum": 86400,
            "description": "Seconds of DVR the live playlist offers"
          },
          "retention_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90,
            "description": "Days the recording is kept (catch-up, start-over, clips, live-to-VOD)"
          },
          "encoders": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Feed letter → encoder host; always has `a` (`b` = the redundant feed)",
            "example": {
              "a": "enc-a"
            }
          },
          "state": {
            "type": "string",
            "enum": [
              "stopped",
              "starting",
              "live",
              "degraded"
            ],
            "description": "Reported by the encoder (not the requested state)"
          },
          "feed_state": {
            "type": "object",
            "description": "Per encoder host: `state` (up | down | unknown), `at` and a free-form `detail` the encoder reports (heartbeat note, session, audio/video levels)",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "state": {
                  "type": "string"
                },
                "at": {
                  "type": "string",
                  "format": "date-time"
                },
                "detail": {
                  "type": "object"
                }
              }
            }
          },
          "deinterlace": {
            "type": "string",
            "enum": [
              "auto",
              "on",
              "off"
            ]
          },
          "ingest": {
            "type": "object",
            "description": "Legacy ingest secrets (rtmp_key, srt_passphrase …). Without channels:operate every non-boolean value is masked as \"***\" (create and PATCH answers are never masked). The sealed SRT passphrase never leaves the API: it shows as `srt_passphrase_set: true` plus `srt_passphrase_set_at`."
          },
          "requested_state": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "live",
              "stopped",
              null
            ],
            "description": "The last state requested through PATCH (the encoder confirms it in `state`)"
          },
          "playback": {
            "type": "object",
            "properties": {
              "live": {
                "type": "string",
                "format": "uri",
                "description": "Live HLS master playlist on the tenant CDN hostname"
              },
              "start_over": {
                "type": "string",
                "description": "template with {programme_id}"
              },
              "catch_up": {
                "type": "string",
                "description": "template with {start}/{end}"
              }
            }
          },
          "recording_status": {
            "type": "object",
            "description": "Channels recorded by two recorders only (GET of one channel): the status the orchestrator last stored (≤ ~30 s old). Absent for single channels and in lists.",
            "properties": {
              "mode": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "ok",
                  "redundancy_lost",
                  "down"
                ]
              },
              "serving_leg": {
                "type": "integer"
              },
              "legs": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "leg": {
                      "type": "integer"
                    },
                    "encoder": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "recording",
                        "behind",
                        "down"
                      ]
                    },
                    "last_segment_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              },
              "changed_at": {
                "type": "string",
                "format": "date-time"
              },
              "checked_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ChannelInput": {
        "type": "object",
        "additionalProperties": false,
        "description": "Create (POST) or partial update (PATCH): only the fields sent change. Unknown fields are rejected (400).",
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
            "description": "required on create; ignored on PATCH (a slug cannot change)"
          },
          "title": {
            "type": "string",
            "maxLength": 200
          },
          "ladder": {
            "type": "string",
            "description": "an existing ladder profile; default the tenant's default ladder"
          },
          "dvr_window_s": {
            "type": "integer",
            "minimum": 60,
            "maximum": 86400,
            "description": "default from the tenant defaults (Settings → Defaults)"
          },
          "retention_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90,
            "description": "default from the tenant defaults (Settings → Defaults)"
          },
          "encoders": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "feed letter → encoder host; must contain \"a\"; default {\"a\": \"enc-a\"}"
          },
          "deinterlace": {
            "type": "string",
            "enum": [
              "auto",
              "on",
              "off"
            ],
            "default": "auto"
          },
          "ingest": {
            "type": "object",
            "description": "legacy ingest secrets (e.g. srt_passphrase, rtmp_key); on PATCH needs channels:operate. The sealed SRT passphrase cannot be set or cleared this way (use POST /v1/channels/{id}/ingest/srt-passphrase)."
          },
          "state": {
            "type": "string",
            "enum": [
              "live",
              "stopped"
            ],
            "description": "PATCH only: requested state (needs channels:operate); the encoder confirms it"
          },
          "policy_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "PATCH only: attach a playback policy (null = inherit the tenant's); needs delivery:write"
          }
        }
      },
      "Programme": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "start_at": {
            "type": "string",
            "format": "date-time"
          },
          "end_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "null = open (still airing / until the next programme)"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "epg",
              "scte35",
              "manual"
            ],
            "description": "epg = imported (replaced by the next import); manual = an editor's or an API marker's; scte35 = from in-band cues"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "meta": {
            "type": [
              "object",
              "null"
            ],
            "description": "Programme metadata (see EPGMeta for the editor's vocabulary; integrations may carry their own keys)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "catchup_exclusion": {
            "$ref": "#/components/schemas/CatchupExclusionStatus"
          },
          "bounds": {
            "$ref": "#/components/schemas/ProgrammeBoundsEffective"
          },
          "adjusted_ms": {
            "type": "integer",
            "format": "int64",
            "description": "List only, when programme boundaries are enabled: play_start − start_at in ms"
          }
        }
      },
      "ProgrammeBoundsEffective": {
        "type": "object",
        "description": "A programme's effective playback window on the recording timeline. Precedence: confirmed (an editor's accept / cut) > detected (the show's opener, the bulletin length) > unverified (EPG + air delay, widened by the channel's early margin) > epg.",
        "properties": {
          "play_start": {
            "type": "string",
            "format": "date-time"
          },
          "play_stop": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "bounds_status": {
            "type": "string",
            "enum": [
              "confirmed",
              "detected",
              "unverified",
              "epg"
            ]
          },
          "bounds_method": {
            "type": "string",
            "description": "how the start was found, e.g. opener, bulletin, offset, prev_end, handover, confirmed"
          },
          "bounds_confidence": {
            "type": "number"
          },
          "air_delay_ms": {
            "type": "integer",
            "description": "the channel's air delay applied"
          },
          "title_mismatch": {
            "type": "object",
            "description": "Another show's opener aired in this slot (the guide names the wrong show)",
            "properties": {
              "series_id": {
                "type": "string",
                "format": "uuid"
              },
              "label": {
                "type": "string"
              }
            }
          }
        }
      },
      "EPGConfig": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "source_url": {
            "type": "string",
            "format": "uri"
          },
          "format": {
            "type": "string",
            "enum": [
              "xmltv",
              "redge_json"
            ]
          },
          "xmltv_channel_id": {
            "type": "string"
          },
          "interval_min": {
            "type": "integer",
            "minimum": 5,
            "maximum": 1440,
            "default": 60
          },
          "daily_at": {
            "type": "string",
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "example": "00:00",
            "description": "also pull once a day at this time (guide timezone, Asia/Jerusalem), whatever the interval"
          },
          "days_ahead": {
            "type": "integer",
            "minimum": 1,
            "maximum": 14,
            "default": 3
          },
          "lang": {
            "type": "string",
            "default": "he"
          }
        }
      },
      "EPGImportResult": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "upload",
              "schedule",
              "manual"
            ]
          },
          "created": {
            "type": "integer"
          },
          "updated": {
            "type": "integer"
          },
          "deleted": {
            "type": "integer"
          },
          "skipped": {
            "type": "integer"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "staged": {
            "type": "boolean",
            "description": "review mode: the changes went to the channel's draft instead of the published guide (omitted when false)"
          },
          "conflicts": {
            "type": "array",
            "description": "Stretches where the source listed contradicting programmes (omitted when none); they also show as source_conflict warnings in validation",
            "items": {
              "type": "object",
              "properties": {
                "from": {
                  "type": "string",
                  "format": "date-time"
                },
                "to": {
                  "type": "string",
                  "format": "date-time"
                },
                "items": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "source_id": {
                        "type": "string",
                        "description": "the programme's id in the source"
                      },
                      "title": {
                        "type": "string"
                      },
                      "start": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "stop": {
                        "type": "string",
                        "format": "date-time",
                        "description": "omitted when the source gives none"
                      }
                    }
                  }
                },
                "held": {
                  "type": "boolean",
                  "description": "true = the stretch kept the guide published before this import; false = nothing was published there yet, so the feed was applied and only this report flags it"
                }
              }
            }
          },
          "held": {
            "type": "integer",
            "description": "source rows not applied because their stretch kept the published guide (omitted when 0)"
          }
        }
      },
      "ChannelEPG": {
        "type": "object",
        "properties": {
          "config": {
            "$ref": "#/components/schemas/EPGConfig"
          },
          "status": {
            "type": "object",
            "description": "The scheduled importer's last run (empty before the first run)",
            "properties": {
              "last_run_at": {
                "type": "string",
                "format": "date-time"
              },
              "last_ok_at": {
                "type": "string",
                "format": "date-time"
              },
              "last_modified": {
                "type": "string",
                "description": "the source's Last-Modified (sent back as If-Modified-Since)"
              },
              "result": {
                "$ref": "#/components/schemas/EPGImportResult"
              },
              "error": {
                "type": "string"
              }
            }
          },
          "export": {
            "type": "object",
            "description": "Public guide exports (GET /epg/…; private when the tenant set an EPG token)",
            "properties": {
              "xmltv": {
                "type": "string",
                "format": "uri"
              },
              "json": {
                "type": "string",
                "format": "uri"
              },
              "tenant_xmltv": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "Collection": {
        "type": "object",
        "description": "A library section; `parent_id` set for a sub-section.",
        "required": [
          "id",
          "customer_id",
          "parent_id",
          "name",
          "slug",
          "sort",
          "item_count",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid",
            "description": "The owning tenant"
          },
          "parent_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The top-level parent section; null for a top-level section"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
            "description": "Unique per tenant"
          },
          "sort": {
            "type": "integer",
            "description": "Order among siblings (ascending, then by name)"
          },
          "item_count": {
            "type": "integer",
            "description": "Assets placed directly in this section (sub-sections not included)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PresetAssignment": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "null for the tenant default"
          },
          "scope": {
            "type": "string",
            "enum": [
              "tenant",
              "live_default",
              "library_default",
              "channel",
              "collection",
              "asset"
            ]
          },
          "target_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "config_id": {
            "type": "string",
            "format": "uuid"
          },
          "config_name": {
            "type": "string"
          },
          "updated_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ResolvedFrom": {
        "type": "object",
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "tenant",
              "live_default",
              "library_default",
              "channel",
              "collection",
              "asset",
              "explicit"
            ],
            "description": "`explicit` = chosen with `?config=` on resolve.json"
          },
          "target_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "target_name": {
            "type": "string",
            "description": "The section name for scope collection; empty otherwise"
          },
          "config_id": {
            "type": "string",
            "format": "uuid"
          },
          "config_name": {
            "type": "string"
          }
        }
      },
      "PlayerConfigDocument": {
        "type": "object",
        "description": "The tenant-editable player configuration (docs/player-configs.md). On write every key is optional and merged; on read every key is present.",
        "additionalProperties": false,
        "properties": {
          "skin": {
            "type": "string",
            "description": "classic | cinema | neon | glass | minimal | emoji | retro, or a skin library id (player.viewstream.co.il/skins, e.g. city)"
          },
          "theme": {
            "type": "string",
            "enum": [
              "auto",
              "light",
              "dark"
            ]
          },
          "lang": {
            "type": "string",
            "enum": [
              "he",
              "en",
              "ru",
              "ar"
            ]
          },
          "big_play_button": {
            "type": "boolean"
          },
          "low_latency": {
            "type": "boolean",
            "description": "live: request LL-HLS (?ll=1)"
          },
          "version_pin": {
            "type": "string",
            "example": "1"
          },
          "branding": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "color": {
                "type": "string",
                "example": "#1FB6FF"
              },
              "accent_fg": {
                "type": "string",
                "description": "Icon/text colour on the accent (#RRGGBB); empty = chosen by the player for contrast"
              },
              "focus": {
                "type": "string",
                "description": "Focus-ring colour (#RRGGBB); empty = the skin's neutral ring"
              },
              "logo": {
                "type": "string",
                "format": "uri"
              },
              "logo_position": {
                "type": "string",
                "enum": [
                  "top-left",
                  "top-right",
                  "bottom-left",
                  "bottom-right"
                ],
                "description": "physical corner (the same in RTL)"
              },
              "logo_enabled": {
                "type": "boolean",
                "description": "false hides the logo and keeps the URL (default true)"
              },
              "logo_width_pct": {
                "type": "integer",
                "minimum": 4,
                "maximum": 30,
                "description": "logo width, % of the player width (default 12)"
              },
              "logo_opacity_pct": {
                "type": "integer",
                "minimum": 10,
                "maximum": 100,
                "description": "default 90"
              },
              "logo_margin_pct": {
                "type": "integer",
                "minimum": 0,
                "maximum": 10,
                "description": "distance from the corner, % of the player width (default 2)"
              },
              "logo_link": {
                "type": "string",
                "format": "uri",
                "description": "optional https click-through (new tab, rel=noopener); empty = not clickable"
              },
              "logo_during_ads": {
                "type": "boolean",
                "description": "keep the logo over video ads (default false: hidden while an ad plays)"
              },
              "watermark": {
                "type": "string",
                "format": "uri"
              },
              "poster_style": {
                "type": "string",
                "enum": [
                  "cover",
                  "contain"
                ]
              }
            }
          },
          "controls": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "autoplay_muted": {
                "type": "boolean"
              },
              "loop": {
                "type": "boolean"
              },
              "speed": {
                "type": "boolean"
              },
              "quality_menu": {
                "type": "boolean"
              },
              "captions": {
                "type": "boolean"
              },
              "chapters": {
                "type": "boolean"
              },
              "share": {
                "type": "boolean"
              },
              "pip": {
                "type": "boolean"
              },
              "cast": {
                "type": "boolean"
              },
              "fullscreen": {
                "type": "boolean"
              },
              "volume": {
                "type": "boolean"
              },
              "seek_buttons": {
                "type": "boolean"
              },
              "stats": {
                "type": "boolean",
                "description": "Stats-for-nerds panel available (settings menu, 'i' key); false removes it"
              }
            }
          },
          "playback": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "start_rung": {
                "type": "string",
                "example": "auto"
              },
              "captions_lang": {
                "type": "string"
              },
              "captions_style": {
                "type": "string",
                "enum": [
                  "default",
                  "large",
                  "high-contrast"
                ]
              },
              "remember_volume": {
                "type": "boolean"
              },
              "resume": {
                "type": "boolean",
                "description": "continue VOD / clips / closed catch-up where the viewer stopped (browser storage only; default true)"
              }
            }
          },
          "live": {
            "type": "object",
            "additionalProperties": false,
            "description": "Live-channel UI (tv10 Redge-style programme panel, programme bar, start-over, previous/next, back-to-live)",
            "properties": {
              "epg_overlay": {
                "type": "boolean"
              },
              "programme_bar": {
                "type": "boolean"
              },
              "start_over_button": {
                "type": "boolean"
              },
              "prev_next_buttons": {
                "type": "boolean"
              },
              "back_to_live_label": {
                "type": "string",
                "maxLength": 40
              },
              "show_channel_name": {
                "type": "boolean"
              },
              "now_badge": {
                "type": "boolean"
              },
              "wall_clock": {
                "type": "boolean"
              },
              "time_zone": {
                "type": "string",
                "enum": [
                  "channel",
                  "viewer"
                ],
                "description": "Live clock and programme times in the channel's time zone (default) or the viewer's"
              },
              "epg_card_position": {
                "type": "string",
                "enum": [
                  "bottom-start",
                  "bottom-end",
                  "top-start",
                  "top-end"
                ],
                "description": "Programme card corner; start/end follow the text direction"
              },
              "max_seek_back_s": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "How far behind live viewers may seek: null = the whole window (default), 0 = live only, 60..86400 seconds"
              },
              "back_to_live_button": {
                "type": "boolean",
                "default": true,
                "description": "false hides the back-to-live button while behind live"
              },
              "restart_button": {
                "type": "boolean",
                "default": false,
                "description": "Restart button on live channels without a guide"
              },
              "restart_back_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 86400,
                "default": 0,
                "description": "How far back the restart button jumps: 0 = the start of the window"
              }
            }
          },
          "vod": {
            "type": "object",
            "description": "On-demand title card: rises with the control bar; data from resolve.json `content`.",
            "properties": {
              "title_overlay": {
                "type": "boolean"
              },
              "show_description": {
                "type": "boolean"
              },
              "show_date": {
                "type": "boolean"
              },
              "show_series": {
                "type": "boolean"
              }
            }
          },
          "ads": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "engine": {
                "type": "string",
                "enum": [
                  "inhouse",
                  "ima"
                ]
              },
              "default_tag": {
                "type": "string",
                "format": "uri"
              },
              "positions": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "pre",
                    "mid",
                    "post"
                  ]
                }
              },
              "mid_interval_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 3600
              },
              "skip_offset_s": {
                "type": "integer",
                "minimum": -1,
                "maximum": 120
              },
              "pre_tag": {
                "type": "string",
                "format": "uri"
              },
              "mid_tag": {
                "type": "string",
                "format": "uri"
              },
              "post_tag": {
                "type": "string",
                "format": "uri"
              },
              "live_tag": {
                "type": "string",
                "format": "uri"
              },
              "max_per_session": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100
              },
              "min_interval_s": {
                "type": "integer",
                "minimum": 0,
                "maximum": 86400
              },
              "timeout_ms": {
                "type": "integer",
                "minimum": 1000,
                "maximum": 15000
              },
              "test_mode": {
                "type": "boolean"
              },
              "personalised": {
                "type": "boolean"
              },
              "source": {
                "type": "string",
                "enum": [
                  "tag",
                  "house"
                ],
                "description": "tag = the VAST/VMAP tags above (default); house = the tenant's own Library video as a pre-roll (ads.house), served as VAST 4.1 by GET /player/config/{tenant}/house-ad/{config_id}.xml — no third-party ad server"
              },
              "house": {
                "type": "object",
                "additionalProperties": false,
                "description": "House pre-roll (pre-roll only; frequency replaces max_per_session / min_interval_s for it)",
                "properties": {
                  "asset_ids": {
                    "type": "array",
                    "maxItems": 5,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "1–5 ready, unprotected Library videos of the tenant with an MP4 rendition, each at most 3 minutes"
                  },
                  "rotation": {
                    "type": "string",
                    "enum": [
                      "round_robin",
                      "random"
                    ],
                    "description": "with several videos: round_robin = the next video at each pre-roll, per viewer (default); random"
                  },
                  "skip_after_s": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 120,
                    "description": "0 = not skippable"
                  },
                  "click_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "optional https click-through"
                  },
                  "frequency": {
                    "type": "string",
                    "enum": [
                      "every_play",
                      "session",
                      "interval"
                    ],
                    "description": "every play, once per browser session, or at most once every interval_min minutes"
                  },
                  "interval_min": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1440
                  },
                  "live": {
                    "type": "boolean",
                    "description": "before live channels (default true)"
                  },
                  "vod": {
                    "type": "boolean",
                    "description": "before library videos, clips and catch-up (default true)"
                  }
                }
              },
              "house_vast_urls": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uri"
                },
                "readOnly": true,
                "description": "computed in the public document when source = house (one VAST URL per house video, ?v=<version>&i=<index>); never stored"
              },
              "allowed_domains": {
                "type": "array",
                "maxItems": 20,
                "description": "Extra ad-server hosts (hostname or *.domain; no IPs, no wildcard directly under a public suffix) allowed in the Studio preview's CSP, on top of the Google IMA / Ad Manager defaults.",
                "items": {
                  "type": "string",
                  "example": "*.adserver.example.com"
                }
              },
              "ssai": {
                "type": "boolean",
                "description": "video-ads phase 2: Player v2 asks catch-up / start-over playlists with ?ssai=1 (server-side ads where the channel turned SSAI on), draws the breaks on the seek bar and plays no client mid-rolls there (default false; independent of enabled)"
              }
            }
          },
          "beacon": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "heartbeat_s": {
                "type": "integer",
                "minimum": 5,
                "maximum": 120
              },
              "viewer_id": {
                "type": "boolean",
                "description": "anonymous first-party viewer id (default true)"
              },
              "viewer_id_consent": {
                "type": "boolean",
                "description": "require consent (Sites banner analytics, embed consent option) before the viewer id is set (default false)"
              },
              "url": {
                "type": "string",
                "readOnly": true,
                "description": "computed in the public document"
              }
            }
          },
          "tile": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "trigger": {
                "type": "string",
                "enum": [
                  "hover",
                  "visible"
                ]
              },
              "max_rung": {
                "type": "string",
                "example": "360p"
              }
            }
          },
          "embed": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "allowed_domains": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "example": [
                  "*.tv10.co.il"
                ]
              }
            }
          }
        }
      },
      "PlayerConfigView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "is_default": {
            "type": "boolean"
          },
          "version": {
            "type": "integer"
          },
          "updated_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "public_url": {
            "type": "string",
            "format": "uri"
          },
          "config": {
            "$ref": "#/components/schemas/PlayerConfigDocument"
          }
        }
      },
      "LiveToVODRule": {
        "type": "object",
        "additionalProperties": false,
        "description": "A channel's live-to-VOD rule. A channel without a rule reads as the defaults below (disabled).",
        "properties": {
          "enabled": {
            "type": "boolean",
            "default": false
          },
          "min_duration_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 43200,
            "default": 300,
            "description": "programmes shorter than this are not published"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "EPG categories to publish; empty = any"
          },
          "title_pattern": {
            "type": "string",
            "description": "Go regular expression matched case-insensitively against the title; empty = any"
          },
          "collection": {
            "type": "string",
            "default": "auto_by_programme_title",
            "description": "`auto_by_programme_title` (a library section named after the programme, created on demand), a section id of the tenant, or empty (none)"
          },
          "publish": {
            "type": "string",
            "enum": [
              "auto",
              "manual"
            ],
            "default": "manual",
            "description": "auto publishes the new asset (needs assets:publish to set)"
          },
          "delay_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 86400,
            "default": 120,
            "description": "seconds after the programme end before it is published; 0 is stored as 120"
          }
        }
      },
      "CatchupItem": {
        "type": "object",
        "description": "One programme of the catch-up list (GET /v1/channels/{id}/catchup)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "start_at": {
            "type": "string",
            "format": "date-time",
            "description": "guide start"
          },
          "end_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "epg",
              "scte35",
              "manual"
            ]
          },
          "duration_s": {
            "type": "integer",
            "description": "length of the playback window so far (to now while airing)"
          },
          "thumbnail": {
            "type": "string",
            "description": "the editor's / automatic poster, else the first recorded frame (absent when nothing is recorded)"
          },
          "poster_mode": {
            "type": "string",
            "enum": [
              "editor",
              "auto"
            ],
            "description": "smart posters: the thumbnail is the editor's or the automatic poster (absent = first frame)"
          },
          "airing": {
            "type": "boolean"
          },
          "coverage": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "recorded share of the playback window"
          },
          "status": {
            "type": "string",
            "enum": [
              "not_recorded",
              "partial",
              "recorded",
              "publishing",
              "published",
              "failed"
            ]
          },
          "vod": {
            "type": [
              "object",
              "null"
            ],
            "description": "the programme's VOD (null = never published)",
            "properties": {
              "asset_id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "status": {
                "type": "string",
                "enum": [
                  "publishing",
                  "published",
                  "failed"
                ]
              },
              "error": {
                "type": "string",
                "description": "redacted failure reason"
              },
              "trigger": {
                "type": "string",
                "enum": [
                  "manual",
                  "bulk",
                  "rule"
                ]
              },
              "start_at": {
                "type": "string",
                "format": "date-time"
              },
              "end_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "play_start": {
            "type": "string",
            "format": "date-time",
            "description": "programme boundaries: effective start on the recording timeline"
          },
          "play_end": {
            "type": "string",
            "format": "date-time"
          },
          "bounds_status": {
            "type": "string",
            "enum": [
              "confirmed",
              "detected",
              "unverified",
              "epg"
            ]
          },
          "bounds_method": {
            "type": "string"
          },
          "adjusted_ms": {
            "type": "integer",
            "format": "int64",
            "description": "play_start − start_at (0 without boundaries)"
          },
          "suggested_start": {
            "type": "string",
            "format": "date-time",
            "description": "unverified only: a detected start awaiting an editor's accept"
          },
          "title_mismatch": {
            "type": "object",
            "properties": {
              "series_id": {
                "type": "string",
                "format": "uuid"
              },
              "label": {
                "type": "string"
              }
            }
          }
        }
      },
      "RecordingTimeline": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "spans": {
            "type": "array",
            "description": "Recorded stretches of [from, to) (gaps under 3 s joined), oldest first",
            "items": {
              "type": "object",
              "properties": {
                "from": {
                  "type": "string",
                  "format": "date-time"
                },
                "to": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "programmes": {
            "type": "array",
            "description": "Programmes overlapping the window, by start",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "start": {
                  "type": "string",
                  "format": "date-time"
                },
                "stop": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "thumbnail": {
                  "type": "string",
                  "format": "uri",
                  "description": "First recorder snapshot at/after the start (absent when none)"
                }
              }
            }
          },
          "poster": {
            "type": "string",
            "format": "uri",
            "description": "The live poster image"
          },
          "thumb_url_template": {
            "type": "string",
            "description": "Snapshot URL with {yyyymmdd} {hhmm} {epoch_s} (UTC; epoch_s a multiple of thumb_interval_s)"
          },
          "thumb_interval_s": {
            "type": "integer",
            "example": 10
          }
        }
      },
      "BoundsMarker": {
        "type": "object",
        "description": "A boundary marker: an audio fingerprint (opener, bulletin sting) or a visual channel ident the detector looks for",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "series_id": {
            "type": "string",
            "format": "uuid",
            "description": "opener only: the show"
          },
          "role": {
            "type": "string",
            "enum": [
              "opener",
              "bulletin",
              "ident"
            ]
          },
          "label": {
            "type": "string"
          },
          "start_offset_ms": {
            "type": "integer",
            "description": "the programme starts this long after the marker"
          },
          "offsets": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "observed start offsets (ms)"
          },
          "hits": {
            "type": "integer"
          },
          "misses": {
            "type": "integer"
          },
          "enabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "duration_ms": {
            "type": "integer"
          },
          "auto": {
            "type": "boolean",
            "description": "cut automatically from a confirmed start (never names a title mismatch)"
          },
          "origin": {
            "type": "string",
            "enum": [
              "manual",
              "confirm",
              "correction",
              "mined"
            ],
            "description": "how it was learned: by an operator, from a confirmed start, from a correction, or mined automatically from the show's recent episodes"
          },
          "snap_only": {
            "type": "boolean",
            "description": "a learned cue that only snaps an opener-derived start"
          },
          "wrong_starts": {
            "type": "integer",
            "description": "corrections that moved a start it produced by more than 5 s"
          },
          "learn": {
            "type": "object",
            "description": "what the learning did with it; a mined marker has `mining` (episodes, support, confidence, per-episode matches) and, for a programme title without a show, `title_key`"
          },
          "down_weighted": {
            "type": "boolean",
            "description": "left out of detection while learning is on (≥ 3 wrong starts, more wrong than hits)"
          }
        }
      },
      "BoundsLearningStatus": {
        "type": "object",
        "description": "Learning from corrections for one channel",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "the channel's opt-out switch (true = learning allowed)"
          },
          "active": {
            "type": "boolean",
            "description": "immediate learning is switched on for this control plane"
          },
          "collecting": {
            "type": "boolean",
            "description": "the correction dataset is being recorded"
          },
          "corrections": {
            "type": "integer",
            "description": "editors' corrections and confirmations"
          },
          "moved": {
            "type": "integer",
            "description": "… of which moved the start by more than 5 s"
          },
          "frame_truths": {
            "type": "integer"
          },
          "bundles_ready": {
            "type": "integer",
            "description": "feature bundles written"
          },
          "last_corrected": {
            "type": "string",
            "format": "date-time"
          },
          "accuracy": {
            "type": "object",
            "description": "detection against frame truths",
            "properties": {
              "checked": {
                "type": "integer"
              },
              "within_5s": {
                "type": "integer"
              },
              "within_60s": {
                "type": "integer"
              },
              "late_over_5s": {
                "type": "integer"
              },
              "median_abs_s": {
                "type": "number"
              },
              "since": {
                "type": "string"
              }
            }
          },
          "last_model": {
            "type": "object",
            "description": "the last boundary-scorer training run",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "type": "string"
              },
              "samples": {
                "type": "integer"
              },
              "metrics": {
                "type": "object"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "finished_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "train_min": {
            "type": "integer",
            "description": "new ready bundles per channel before a training run"
          },
          "retention_days": {
            "type": "integer",
            "description": "dataset retention"
          },
          "markers": {
            "type": "array",
            "description": "markers the learning touched or made (including openers mined automatically)",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/BoundsMarker"
                },
                {
                  "type": "object",
                  "properties": {
                    "what": {
                      "type": "string",
                      "enum": [
                        "cue",
                        "cold_open",
                        "down_weighted",
                        "rejected",
                        "mined",
                        "wrong_starts"
                      ],
                      "description": "mined = learned automatically from the show's recent episodes (disable it with PATCH /v1/channels/{id}/bounds/markers/{mid})"
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "BoundsChannelSettings": {
        "type": "object",
        "description": "A channel's programme-boundary settings (zero values until first saved; early_margin_ms is 90000 once a row exists)",
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "enabled": {
            "type": "boolean",
            "description": "boundary detection on"
          },
          "air_delay_ms": {
            "type": "integer",
            "minimum": -600000,
            "maximum": 600000,
            "description": "the source's delay behind real air time"
          },
          "air_delay_auto": {
            "type": "boolean",
            "description": "measure the air delay from the on-air clock"
          },
          "clock": {
            "type": "object",
            "description": "on-air clock for OCR calibration: {crop:[x,y,w,h], rendition, format}"
          },
          "measured_at": {
            "type": "string",
            "format": "date-time",
            "description": "when the air delay was last measured"
          },
          "early_margin_ms": {
            "type": "integer",
            "minimum": 0,
            "maximum": 600000,
            "description": "an unverified start plays this much before guide time + delay, and its end this much after"
          }
        }
      },
      "EPGMeta": {
        "type": "object",
        "description": "Rich programme metadata (programmes.meta). Every field optional; unknown fields are rejected on write.",
        "properties": {
          "description": {
            "type": "string",
            "description": "Hebrew (or `lang`) description"
          },
          "description_en": {
            "type": "string"
          },
          "title_en": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "genres": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "news",
                "current_affairs",
                "business",
                "talk",
                "magazine",
                "documentary",
                "sport",
                "entertainment",
                "drama",
                "film",
                "kids",
                "music",
                "education",
                "lifestyle",
                "religion",
                "weather"
              ]
            }
          },
          "image": {
            "type": "string",
            "format": "uri",
            "description": "16:9 image (https)"
          },
          "images": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            },
            "description": "per aspect: 16x9, 2x3, 1x1"
          },
          "episode": {
            "type": "string",
            "description": "free-form on-screen episode label"
          },
          "season": {
            "type": "integer",
            "minimum": 1
          },
          "episode_num": {
            "type": "integer",
            "minimum": 1
          },
          "episode_title": {
            "type": "string"
          },
          "rating": {
            "type": "string",
            "enum": [
              "all",
              "8",
              "12",
              "14",
              "16",
              "18"
            ],
            "description": "Israeli age rating"
          },
          "presenters": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "guests": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "flags": {
            "type": "object",
            "properties": {
              "live": {
                "type": "boolean"
              },
              "rerun": {
                "type": "boolean"
              },
              "premiere": {
                "type": "boolean"
              },
              "news": {
                "type": "boolean"
              },
              "sign_language": {
                "type": "boolean"
              },
              "subtitles": {
                "type": "boolean"
              }
            }
          },
          "rights": {
            "type": "object",
            "description": "Absent / null = allowed. false excludes the programme: catch-up and start-over manifests answer 403, Sites and the player hide the buttons, live-to-VOD refuses to publish.",
            "properties": {
              "catchup": {
                "type": "boolean"
              },
              "start_over": {
                "type": "boolean"
              },
              "vod": {
                "type": "boolean"
              }
            }
          },
          "lang": {
            "type": "string"
          }
        }
      },
      "EPGCatalog": {
        "type": "object",
        "properties": {
          "genres": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "en": {
                  "type": "string"
                },
                "he": {
                  "type": "string"
                }
              }
            }
          },
          "ratings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "en": {
                  "type": "string"
                },
                "he": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "EPGWorkflow": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "mode": {
            "type": "string",
            "enum": [
              "auto",
              "review"
            ]
          },
          "lock_hours": {
            "type": "integer"
          },
          "auto_publish_imports": {
            "type": "boolean",
            "description": "review mode: scheduled imports still publish directly (default true)"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "when the workflow was last set (zero time for a channel on the defaults)"
          },
          "draft_state": {
            "type": "string",
            "enum": [
              "empty",
              "draft",
              "in_review"
            ],
            "description": "GET/PUT …/epg/workflow only"
          },
          "draft_ops": {
            "type": "integer",
            "description": "GET/PUT …/epg/workflow only: staged operations"
          },
          "can_publish": {
            "type": "boolean",
            "description": "GET/PUT …/epg/workflow only: the caller has epg:publish or channels:write"
          }
        }
      },
      "EPGRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "start_at": {
            "type": "string",
            "format": "date-time"
          },
          "end_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "epg",
              "scte35",
              "manual"
            ]
          },
          "external_id": {
            "type": "string"
          },
          "meta": {
            "$ref": "#/components/schemas/EPGMeta"
          },
          "status": {
            "type": "string",
            "enum": [
              "added",
              "changed",
              "removed"
            ],
            "description": "draft view only"
          }
        }
      },
      "EPGOpInput": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "create",
              "update",
              "delete"
            ]
          },
          "programme_id": {
            "type": "string",
            "format": "uuid",
            "description": "update/delete: a programme of the channel or one created earlier in the draft"
          },
          "data": {
            "type": "object",
            "properties": {
              "start_at": {
                "type": "string",
                "format": "date-time"
              },
              "end_at": {
                "type": "string",
                "format": "date-time"
              },
              "open_end": {
                "type": "boolean"
              },
              "title": {
                "type": "string"
              },
              "meta": {
                "$ref": "#/components/schemas/EPGMeta"
              },
              "source": {
                "type": "string",
                "enum": [
                  "epg",
                  "manual",
                  "scte35"
                ],
                "description": "programmes.source of the result"
              },
              "external_id": {
                "type": "string",
                "description": "create only (ignored on update)"
              }
            }
          }
        }
      },
      "EPGOp": {
        "type": "object",
        "description": "A staged operation of a draft (as returned in `EPGDraft.ops`)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "operation id (DELETE …/epg/draft/ops/{oid})"
          },
          "kind": {
            "type": "string",
            "enum": [
              "create",
              "update",
              "delete"
            ]
          },
          "programme_id": {
            "type": "string",
            "format": "uuid",
            "description": "create: the id the new programme will get"
          },
          "data": {
            "type": "object",
            "properties": {
              "start_at": {
                "type": "string",
                "format": "date-time"
              },
              "end_at": {
                "type": "string",
                "format": "date-time"
              },
              "open_end": {
                "type": "boolean"
              },
              "title": {
                "type": "string"
              },
              "meta": {
                "$ref": "#/components/schemas/EPGMeta"
              },
              "source": {
                "type": "string",
                "enum": [
                  "epg",
                  "manual",
                  "scte35"
                ]
              },
              "external_id": {
                "type": "string"
              }
            }
          },
          "source": {
            "type": "string",
            "enum": [
              "editor",
              "import",
              "copy",
              "template",
              "rollback",
              "fill"
            ],
            "description": "what staged it"
          },
          "author": {
            "type": "string",
            "format": "uuid",
            "description": "user or API key id (absent for imports)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EPGDiff": {
        "type": "object",
        "properties": {
          "added": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGRow"
            }
          },
          "removed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGRow"
            }
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "fields": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "before": {
                  "$ref": "#/components/schemas/EPGRow"
                },
                "after": {
                  "$ref": "#/components/schemas/EPGRow"
                }
              }
            }
          }
        }
      },
      "EPGSummary": {
        "type": "object",
        "properties": {
          "added": {
            "type": "integer"
          },
          "changed": {
            "type": "integer"
          },
          "removed": {
            "type": "integer"
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EPGIssue": {
        "type": "object",
        "properties": {
          "level": {
            "type": "string",
            "enum": [
              "error",
              "warning"
            ]
          },
          "code": {
            "type": "string",
            "enum": [
              "overlap",
              "gap",
              "short",
              "missing_title",
              "missing_image",
              "missing_rating",
              "source_conflict"
            ],
            "description": "overlap and missing_title are errors (they block publishing a draft); source_conflict = the last import found contradictory programmes in the source"
          },
          "programme_id": {
            "type": "string",
            "format": "uuid",
            "description": "absent when the issue is not tied to one programme"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "message": {
            "type": "string",
            "description": "English; clients that render their own language use code + params"
          },
          "params": {
            "type": "object",
            "additionalProperties": true,
            "description": "The message's values (added 2026-09-29): title, other_title (overlap), seconds (gap/overlap/short), field (what fixes it: title | image | rating | start | end)"
          }
        }
      },
      "EPGFixImage": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "poster",
              "still"
            ]
          },
          "source_programme_id": {
            "type": "string",
            "format": "uuid"
          },
          "source_start_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EPGFixPreview": {
        "type": "object",
        "properties": {
          "programme_id": {
            "type": "string",
            "format": "uuid"
          },
          "field": {
            "type": "string",
            "enum": [
              "image",
              "rating"
            ]
          },
          "image": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EPGFixImage"
              }
            ]
          },
          "series": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string"
              },
              "programmes": {
                "type": "integer"
              },
              "templates": {
                "type": "integer"
              },
              "from": {
                "type": "string",
                "format": "date-time"
              },
              "to": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "default": {
            "$ref": "#/components/schemas/EPGSeriesDefault"
          },
          "can_default": {
            "type": "boolean"
          }
        }
      },
      "EPGFixResult": {
        "type": "object",
        "properties": {
          "staged": {
            "type": "boolean"
          },
          "version": {
            "$ref": "#/components/schemas/EPGVersion"
          },
          "ops": {
            "type": "integer"
          },
          "programmes": {
            "type": "integer"
          },
          "templates": {
            "type": "integer"
          },
          "default_saved": {
            "type": "boolean"
          },
          "image": {
            "$ref": "#/components/schemas/EPGFixImage"
          }
        }
      },
      "EPGSeriesDefault": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "series_key": {
            "type": "string",
            "description": "the title lower-cased with whitespace collapsed; how programmes are matched to the series"
          },
          "title": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "description": "absent when only a rating is stored"
          },
          "rating": {
            "type": "string",
            "enum": [
              "all",
              "8",
              "12",
              "14",
              "16",
              "18"
            ],
            "description": "absent when only an image is stored"
          },
          "updated_by": {
            "type": "string",
            "description": "who stored it: user:<email>, key:<key prefix> or interhost:<operator>"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EPGDraft": {
        "type": "object",
        "properties": {
          "workflow": {
            "$ref": "#/components/schemas/EPGWorkflow"
          },
          "state": {
            "type": "string",
            "enum": [
              "empty",
              "draft",
              "in_review"
            ]
          },
          "submitted_by": {
            "type": "string",
            "format": "uuid",
            "description": "user or API key id that submitted it (in_review only)"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "note": {
            "type": "string"
          },
          "ops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGOp"
            }
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGRow"
            },
            "description": "the window as it will be after publishing; removed rows included, flagged"
          },
          "diff": {
            "$ref": "#/components/schemas/EPGDiff"
          },
          "summary": {
            "$ref": "#/components/schemas/EPGSummary"
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGIssue"
            }
          },
          "touches_lock": {
            "type": "boolean",
            "description": "publishing needs ?confirm_lock=1"
          },
          "can_publish": {
            "type": "boolean"
          }
        }
      },
      "EPGVersion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "n": {
            "type": "integer"
          },
          "source": {
            "type": "string",
            "enum": [
              "publish",
              "import",
              "edit",
              "copy",
              "template",
              "rollback"
            ]
          },
          "author": {
            "type": "string",
            "format": "uuid",
            "description": "user or API key id (absent for imports)"
          },
          "note": {
            "type": "string"
          },
          "summary": {
            "$ref": "#/components/schemas/EPGSummary"
          },
          "diff": {
            "$ref": "#/components/schemas/EPGDiff",
            "description": "full diff on GET …/versions/{n} and publish/rollback responses; arrays are null in the version list"
          },
          "rollback_of": {
            "type": "integer",
            "description": "rollback versions: the version undone"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EPGCommitResult": {
        "type": "object",
        "properties": {
          "staged": {
            "type": "boolean",
            "description": "review mode: the change is in the draft"
          },
          "version": {
            "$ref": "#/components/schemas/EPGVersion"
          },
          "ops": {
            "type": "integer"
          }
        }
      },
      "EPGRollbackPreview": {
        "type": "object",
        "description": "Dry run of a rollback: what publishing it would change",
        "properties": {
          "diff": {
            "$ref": "#/components/schemas/EPGDiff"
          },
          "summary": {
            "$ref": "#/components/schemas/EPGSummary"
          },
          "touches_lock": {
            "type": "boolean",
            "description": "publishing needs ?confirm_lock=1"
          }
        }
      },
      "EPGTemplate": {
        "type": "object",
        "required": [
          "title",
          "weekdays",
          "start_time",
          "duration_min"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "readOnly": true
          },
          "channel_id": {
            "type": "string",
            "format": "uuid",
            "readOnly": true
          },
          "title": {
            "type": "string"
          },
          "weekdays": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 6
            },
            "description": "0 = Sunday … 6 = Saturday"
          },
          "start_time": {
            "type": "string",
            "example": "09:00",
            "description": "HH:MM in the guide time zone"
          },
          "duration_min": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1440
          },
          "meta": {
            "$ref": "#/components/schemas/EPGMeta"
          },
          "enabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          }
        }
      },
      "EPGDestinationTarget": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "https: POST target (signed with X-VS-Signature like webhooks)"
          },
          "host": {
            "type": "string",
            "description": "sftp"
          },
          "port": {
            "type": "integer",
            "description": "sftp (22)"
          },
          "user": {
            "type": "string",
            "description": "sftp"
          },
          "path": {
            "type": "string",
            "description": "sftp: absolute remote file (written as.part, then renamed); s3: object key"
          },
          "host_key": {
            "type": "string",
            "description": "sftp: expected host key (authorized_keys line); empty = pinned on first use"
          },
          "endpoint": {
            "type": "string",
            "format": "uri",
            "description": "s3"
          },
          "region": {
            "type": "string",
            "description": "s3"
          },
          "bucket": {
            "type": "string",
            "description": "s3"
          },
          "eit": {
            "$ref": "#/components/schemas/EPGEITConfig"
          }
        }
      },
      "EPGChannelMap": {
        "type": "object",
        "required": [
          "channel_id"
        ],
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": "string",
            "description": "the partner's channel id (default <slug>.<tenant>.viewstream.co.il)"
          },
          "service_id": {
            "type": "integer",
            "minimum": 1,
            "maximum": 65535,
            "description": "eit: the service_id in the partner's transport stream (required)"
          },
          "transport_stream_id": {
            "type": "integer",
            "minimum": 0,
            "maximum": 65535,
            "description": "eit (default 0)"
          },
          "original_network_id": {
            "type": "integer",
            "minimum": 0,
            "maximum": 65535,
            "description": "eit (default 0)"
          }
        }
      },
      "EPGEITConfig": {
        "type": "object",
        "description": "kind eit: DVB EIT (EN 300 468) present/following + schedule of the published guide",
        "properties": {
          "encoding": {
            "type": "string",
            "enum": [
              "iso-8859-8",
              "iso-8859-8-1byte",
              "utf-8"
            ],
            "default": "iso-8859-8",
            "description": "Hebrew text: ISO/IEC 8859-8 with selector 0x10 0x00 0x08 (default, what Israeli receivers decode), the same table with the one-byte selector 0x04, or UTF-8 (0x15)"
          },
          "schedule_days": {
            "type": "integer",
            "minimum": 0,
            "maximum": 8,
            "default": 8,
            "description": "EIT schedule days (0 = present/following only)"
          },
          "stream": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "url": {
                "type": "string",
                "description": "udp://host:port or rtp://host:port (SSRF guard; private/multicast only via the operator allow-list EIT_STREAM_ALLOW)"
              },
              "ttl": {
                "type": "integer",
                "minimum": 0,
                "maximum": 255,
                "default": 16
              }
            }
          }
        }
      },
      "EPGEITReport": {
        "type": "object",
        "description": "The EIT rendered now, re-parsed from its own transport stream",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "encoding": {
            "type": "string"
          },
          "packets": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "sections": {
            "type": "integer"
          },
          "events": {
            "type": "integer"
          },
          "lossy_characters": {
            "type": "integer"
          },
          "skipped_programmes": {
            "type": "integer"
          },
          "dropped_events": {
            "type": "integer"
          },
          "ok": {
            "type": "boolean"
          },
          "checks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "ok": {
                  "type": "boolean"
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          },
          "tables": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "table_id": {
                  "type": "string"
                },
                "service_id": {
                  "type": "integer"
                },
                "version": {
                  "type": "integer"
                },
                "sections": {
                  "type": "integer"
                },
                "events": {
                  "type": "integer"
                },
                "max_section_bytes": {
                  "type": "integer"
                }
              }
            }
          },
          "present_following": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGEITEvent"
            }
          },
          "sample": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGEITEvent"
            }
          }
        }
      },
      "EPGEITEvent": {
        "type": "object",
        "description": "An EIT event read back from the rendered transport stream",
        "properties": {
          "event_id": {
            "type": "integer"
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "duration": {
            "type": "string",
            "description": "e.g. 1h0m0s"
          },
          "running_status": {
            "type": "string"
          },
          "names": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "ISO 639 language → event_name"
          },
          "short_text": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "extended_text": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "content": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "content_descriptor nibbles, e.g. 0x20"
          },
          "rating": {
            "type": "string",
            "description": "e.g. ISR 0x0D (16+)"
          }
        }
      },
      "EPGDestinationInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "pull",
              "https",
              "sftp",
              "s3",
              "eit"
            ],
            "description": "cannot change after create; eit = DVB EIT for a playout/mux (download + optional UDP/RTP stream)"
          },
          "format": {
            "type": "string",
            "enum": [
              "xmltv",
              "json",
              "eit"
            ],
            "description": "eit for kind eit"
          },
          "channels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGChannelMap"
            }
          },
          "back_days": {
            "type": "integer",
            "minimum": 0,
            "maximum": 14,
            "default": 1
          },
          "ahead_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 21,
            "default": 7
          },
          "schedule": {
            "type": "string",
            "enum": [
              "on_publish",
              "interval",
              "manual"
            ],
            "default": "on_publish"
          },
          "every_min": {
            "type": "integer",
            "minimum": 5,
            "maximum": 1440,
            "default": 60
          },
          "target": {
            "$ref": "#/components/schemas/EPGDestinationTarget"
          },
          "credentials": {
            "type": "object",
            "writeOnly": true,
            "description": "sealed at rest, never returned",
            "properties": {
              "secret": {
                "type": "string"
              },
              "password": {
                "type": "string"
              },
              "private_key": {
                "type": "string"
              },
              "access_key": {
                "type": "string"
              },
              "secret_key": {
                "type": "string"
              }
            }
          },
          "enabled": {
            "type": "boolean"
          }
        }
      },
      "EPGDestination": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "pull",
              "https",
              "sftp",
              "s3",
              "eit"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "xmltv",
              "json",
              "eit"
            ]
          },
          "channels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPGChannelMap"
            }
          },
          "back_days": {
            "type": "integer"
          },
          "ahead_days": {
            "type": "integer"
          },
          "schedule": {
            "type": "string",
            "enum": [
              "on_publish",
              "interval",
              "manual"
            ]
          },
          "every_min": {
            "type": "integer"
          },
          "target": {
            "$ref": "#/components/schemas/EPGDestinationTarget"
          },
          "has_credentials": {
            "type": "boolean"
          },
          "token_hint": {
            "type": "string",
            "description": "pull: the first 10 characters of the token, e.g. epg_Q2x9vK…"
          },
          "feed_url": {
            "type": "string",
            "description": "pull: the feed URL ({token} unless just issued)"
          },
          "token": {
            "type": "string",
            "description": "pull: only in the create / rotate response"
          },
          "enabled": {
            "type": "boolean"
          },
          "pending_since": {
            "type": "string",
            "format": "date-time",
            "description": "on_publish: a new version is waiting to be delivered (after a minute of quiet)"
          },
          "last_run_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_ok_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_status": {
            "type": "string",
            "enum": [
              "ok",
              "failed"
            ]
          },
          "last_error": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EPGDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "destination_id": {
            "type": "string",
            "format": "uuid"
          },
          "trigger": {
            "type": "string",
            "enum": [
              "schedule",
              "publish",
              "manual",
              "pull",
              "stream"
            ]
          },
          "note": {
            "type": "string",
            "description": "eit stream: the present/following version and target"
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "failed"
            ]
          },
          "programmes": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "duration_ms": {
            "type": "integer"
          },
          "error": {
            "type": "string"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EPGFeed": {
        "type": "object",
        "properties": {
          "generator": {
            "type": "string"
          },
          "generated": {
            "type": "string",
            "format": "date-time"
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "slug": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "programmes": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "start": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "stop": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      },
                      "title": {
                        "type": "string"
                      },
                      "meta": {
                        "$ref": "#/components/schemas/EPGMeta"
                      },
                      "catchup": {
                        "type": "boolean"
                      },
                      "start_over": {
                        "type": "string",
                        "format": "uri"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "EPGExportChannel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "poster": {
            "type": "string",
            "format": "uri",
            "description": "live poster = the latest frame, refreshed every 10 s (platform tenants)"
          },
          "programmes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "start": {
                  "type": "string",
                  "format": "date-time"
                },
                "stop": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "title": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                },
                "image": {
                  "type": "string"
                },
                "start_over": {
                  "type": "string",
                  "format": "uri",
                  "description": "omitted when the programme's rights forbid start-over / catch-up"
                },
                "catchup": {
                  "type": "boolean",
                  "description": "rights: may be watched after it aired"
                },
                "meta": {
                  "$ref": "#/components/schemas/EPGMeta"
                },
                "play_start": {
                  "type": "string",
                  "format": "date-time",
                  "description": "programme-boundaries: where the programme really starts on the recording timeline (start-over allowed and boundaries known)"
                },
                "play_stop": {
                  "type": "string",
                  "format": "date-time"
                },
                "bounds_status": {
                  "type": "string",
                  "enum": [
                    "confirmed",
                    "detected",
                    "unverified",
                    "epg"
                  ],
                  "description": "how play_start/play_stop were decided"
                },
                "thumbnail": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uri",
                  "description": "the programme's poster: the editor's choice or the automatic best still (smart posters), else its first frame (recorder snapshot); null for future or unrecorded programmes"
                }
              }
            }
          }
        }
      },
      "ProgrammeInput": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "start_at"
        ],
        "properties": {
          "start_at": {
            "type": "string",
            "format": "date-time"
          },
          "end_at": {
            "type": "string",
            "format": "date-time",
            "description": "after start_at; omit for an open programme"
          },
          "title": {
            "type": "string",
            "description": "required for an editor's new manual programme (manual, no external_id) when EPG publishing is on"
          },
          "external_id": {
            "type": "string",
            "description": "upsert key of an integration's programme"
          },
          "source": {
            "type": "string",
            "enum": [
              "epg",
              "scte35",
              "manual"
            ],
            "default": "epg"
          },
          "meta": {
            "type": "object",
            "description": "free-form for integrations; an editor's manual programme (manual, no external_id) is checked against the EPGMeta vocabulary"
          }
        }
      },
      "ProgrammeVOD": {
        "type": "object",
        "description": "A programme published (or being published) as a VOD asset",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "programme_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "programme_start": {
            "type": "string",
            "format": "date-time"
          },
          "start_at": {
            "type": "string",
            "format": "date-time",
            "description": "the published range (trimmed bounds)"
          },
          "end_at": {
            "type": "string",
            "format": "date-time"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "publishing",
              "published",
              "failed"
            ]
          },
          "asset_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "job_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "collection_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "publish": {
            "type": "boolean"
          },
          "trigger": {
            "type": "string",
            "enum": [
              "manual",
              "bulk",
              "rule"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Clip": {
        "type": "object",
        "required": [
          "id",
          "customer_id",
          "status",
          "precision",
          "renditions",
          "playback",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "asset_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Reserved for asset clips (not offered yet; always null)"
          },
          "start_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "end_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "start_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Reserved for asset clips (always null)"
          },
          "end_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Reserved for asset clips (always null)"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "ready",
              "finalizing",
              "final",
              "failed",
              "deleted"
            ],
            "description": "segment precision: ready; frame precision: finalizing → final | failed; deleted after DELETE"
          },
          "precision": {
            "type": "string",
            "enum": [
              "segment",
              "frame"
            ]
          },
          "renditions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Rendition labels in the clip (e.g. 1080p, 720p, aac-128)"
          },
          "finalize_job_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "provenance": {
            "type": "object",
            "description": "Who cut it: request_id and the API key prefix or the Studio user"
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "final_plan": {
            "type": "object",
            "description": "Frame-precision clips once final: the clip_finalize job result (per-rendition segments of the exact cut) the manifest service plays"
          },
          "variants": {
            "type": "object",
            "description": "Files made from the clip besides its HLS renditions, by name (omitted when none). `vertical-9x16`: the 1080×1920 MP4 with burned-in Hebrew captions and the tenant's title card, made by POST /v1/ai/clips/{id}/render (AI clips); clip.final carries the variants it has.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "status": {
                  "type": "string",
                  "enum": [
                    "queued",
                    "ready",
                    "failed"
                  ]
                },
                "url": {
                  "type": "string",
                  "description": "the MP4 on the tenant's delivery hostname"
                },
                "poster_url": {
                  "type": "string"
                },
                "width": {
                  "type": "integer"
                },
                "height": {
                  "type": "integer"
                },
                "duration_ms": {
                  "type": "integer",
                  "format": "int64"
                },
                "bytes": {
                  "type": "integer",
                  "format": "int64"
                },
                "artifact_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "the AI artefact it was rendered from"
                },
                "job_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "error": {
                  "type": "string"
                },
                "rendered_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "ready_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "final_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Only for a ready (segment-precision) channel clip: end_at + the channel's retention, when its recording ages out"
          },
          "playback": {
            "type": "object",
            "required": [
              "hls"
            ],
            "properties": {
              "hls": {
                "type": "string",
                "format": "uri",
                "description": "Clip master playlist on the tenant's CDN hostname (`/m/clips/<id>/master.m3u8?c=<tenant>`)"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ClipInput": {
        "type": "object",
        "required": [
          "channel_id",
          "start_at",
          "end_at"
        ],
        "additionalProperties": false,
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid",
            "description": "A channel of the tenant that is not in the trash"
          },
          "start_at": {
            "type": "string",
            "format": "date-time"
          },
          "end_at": {
            "type": "string",
            "format": "date-time",
            "description": "After start_at and ≤ 6 h after it"
          },
          "title": {
            "type": "string"
          },
          "precision": {
            "type": "string",
            "enum": [
              "segment",
              "frame"
            ],
            "default": "segment",
            "description": "segment = ready at once on the recording's segments; frame = exact cut by a clip_finalize job"
          },
          "renditions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Subset of the channel's ladder labels, each recorded in the range; default every recorded rendition"
          },
          "publish": {
            "type": "boolean",
            "description": "true requires scope clips:publish (checked only; clips have no separate publish state)"
          },
          "asset_id": {
            "type": "string",
            "format": "uuid",
            "description": "Not supported yet — any value is a 422 (channel clips only)"
          },
          "start_ms": {
            "type": "integer",
            "description": "Reserved for asset clips; ignored"
          },
          "end_ms": {
            "type": "integer",
            "description": "Reserved for asset clips; ignored"
          }
        }
      },
      "SLAMonthly": {
        "type": "object",
        "description": "Monthly SLA report of a channel (v3 15.2; GET /v1/channels/{id}/sla/monthly).",
        "properties": {
          "tenant": {
            "type": "string"
          },
          "channel": {
            "type": "string"
          },
          "channel_title": {
            "type": "string"
          },
          "month": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "partial": {
            "type": "boolean",
            "description": "the month has not ended"
          },
          "target_pct": {
            "type": "string"
          },
          "eligible_minutes": {
            "type": "integer"
          },
          "sla_pct": {
            "type": "string",
            "description": "(eligible − platform down) ÷ eligible × 100, 3 decimals"
          },
          "met": {
            "type": "boolean"
          },
          "credit_pct": {
            "type": "string"
          },
          "credit_tier": {
            "type": "object",
            "properties": {
              "below_pct": {
                "type": "string"
              },
              "credit_pct": {
                "type": "string"
              }
            }
          },
          "credit_schedule": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "maintenance": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "exclusions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "statement": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "type": "string"
              },
              "currency": {
                "type": "string"
              },
              "subtotal": {
                "type": "string"
              },
              "credit_amount": {
                "type": "string"
              }
            }
          },
          "report": {
            "$ref": "#/components/schemas/ChannelSLA"
          }
        }
      },
      "SLAShare": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "channel": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "sla",
              "events"
            ]
          },
          "month": {
            "type": [
              "string",
              "null"
            ]
          },
          "from": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "to": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lang": {
            "type": "string",
            "enum": [
              "he",
              "en"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "uses": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "description": "only in the create answer"
          }
        }
      },
      "ChannelSLA": {
        "type": "object",
        "description": "Availability and events of a channel over a period.",
        "required": [
          "channel_id",
          "from",
          "to",
          "rule",
          "minutes",
          "down_minutes",
          "down_minutes_source",
          "down_minutes_platform",
          "availability_pct",
          "periods",
          "events"
        ],
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "from": {
            "type": "string",
            "format": "date-time",
            "description": "period start (truncated to the minute)"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "rule": {
            "type": "string",
            "description": "the down-minute rule in force"
          },
          "minutes": {
            "type": "integer",
            "description": "minutes measured (from the channel's first recording in the period)"
          },
          "down_minutes": {
            "type": "integer"
          },
          "down_minutes_source": {
            "type": "integer",
            "description": "down because of the contribution feed (slate on air / feed_down alert)"
          },
          "down_minutes_platform": {
            "type": "integer",
            "description": "down while the feed was not reported down (not recorded)"
          },
          "down_minutes_maintenance": {
            "type": "integer",
            "description": "down inside an announced maintenance window (v3 15.2; only with SLA terms)"
          },
          "minutes_gate3": {
            "type": "integer",
            "description": "minutes decided by the external probes under the Gate 3 rule (v3 15.1); the rest used `rule`"
          },
          "rule_gate3": {
            "type": "string",
            "description": "the Gate 3 rule, present when minutes_gate3 > 0"
          },
          "availability_pct": {
            "type": "number",
            "description": "(minutes − down_minutes) / minutes × 100, 3 decimals; 0 when nothing was measured"
          },
          "periods": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "start",
                "end",
                "minutes",
                "cause"
              ],
              "properties": {
                "start": {
                  "type": "string",
                  "format": "date-time"
                },
                "end": {
                  "type": "string",
                  "format": "date-time"
                },
                "minutes": {
                  "type": "integer"
                },
                "cause": {
                  "type": "string",
                  "enum": [
                    "source",
                    "platform",
                    "maintenance"
                  ]
                }
              }
            }
          },
          "events": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "at",
                "kind",
                "source"
              ],
              "properties": {
                "at": {
                  "type": "string",
                  "format": "date-time"
                },
                "end": {
                  "type": "string",
                  "format": "date-time",
                  "description": "alerts: when it resolved (omitted while open)"
                },
                "kind": {
                  "type": "string",
                  "description": "alert check type (feed_down, recorder_gap, …) or feed event kind (slate_on, slate_off, failover, switch, return, …)"
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "alert",
                    "feed"
                  ]
                },
                "severity": {
                  "type": "string"
                },
                "title_he": {
                  "type": "string"
                },
                "title_en": {
                  "type": "string"
                },
                "detail": {
                  "type": "string",
                  "description": "feed events: feed_from→feed_to and the actor"
                }
              }
            }
          }
        }
      },
      "ChannelAdSettings": {
        "type": "object",
        "description": "video-ads phase 2 — how a channel learns its ad breaks and whether its playlists may stitch server-side ads. A channel never configured shows the defaults (cue_source none, SSAI off, vast_timeout_ms 2000).",
        "required": [
          "channel_id",
          "cue_source",
          "cue_url",
          "cue_offset_ms",
          "effective_offset_ms",
          "ssai_enabled",
          "vast_tag",
          "vast_timeout_ms",
          "polled_at",
          "poll_error",
          "updated_by",
          "updated_at"
        ],
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "cue_source": {
            "type": "string",
            "enum": [
              "none",
              "redge_dai",
              "scte35",
              "manual"
            ],
            "description": "redge_dai = poll the broadcaster's Redge DAI manifest; manual = editors add breaks; scte35 = reserved"
          },
          "cue_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "redge_dai: the DAI DASH manifest (…/dai.livx)"
          },
          "cue_offset_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null = the channel's air delay"
          },
          "effective_offset_ms": {
            "type": "integer",
            "description": "what maps cue times onto the recording timeline (cue_offset_ms, else the air delay, else 0)"
          },
          "ssai_enabled": {
            "type": "boolean"
          },
          "vast_tag": {
            "type": [
              "string",
              "null"
            ],
            "description": "VAST request URL with macros"
          },
          "vast_timeout_ms": {
            "type": "integer"
          },
          "decisioning": {
            "type": "string",
            "enum": [
              "vast",
              "gam",
              "freewheel",
              "adocean"
            ],
            "description": "v3 decisioning adapter (default vast)"
          },
          "decisioning_params": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "fallback_tag": {
            "type": [
              "string",
              "null"
            ]
          },
          "freq_cap_per_hour": {
            "type": [
              "integer",
              "null"
            ]
          },
          "contextual_kv": {
            "type": "boolean"
          },
          "polled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "last poll of cue_url"
          },
          "poll_error": {
            "type": [
              "string",
              "null"
            ],
            "description": "last poll error (null after a success, or after the cue source / URL changed)"
          },
          "poll_state": {
            "type": "object",
            "description": "the cue poller's last state; omitted before the first poll",
            "properties": {
              "period_id": {
                "type": "string"
              },
              "period_start": {
                "type": "string",
                "format": "date-time"
              },
              "in_break": {
                "type": "boolean"
              },
              "breaks": {
                "type": "integer",
                "description": "breaks recorded since the URL was set"
              }
            }
          },
          "updated_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "DRMSettingsView": {
        "type": "object",
        "description": "v3/ — DRM as a service switches and endpoint URLs",
        "required": [
          "settings",
          "endpoints",
          "fairplay_ksm_configured"
        ],
        "properties": {
          "settings": {
            "type": "object",
            "required": [
              "service_enabled",
              "leak_key_datacenter"
            ],
            "properties": {
              "service_enabled": {
                "type": "boolean"
              },
              "leak_key_datacenter": {
                "type": "boolean"
              },
              "updated_by": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "updated_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "endpoints": {
            "type": "object",
            "properties": {
              "speke_v2": {
                "type": "string"
              },
              "fairplay_certificate": {
                "type": "string"
              },
              "fairplay_licence": {
                "type": "string"
              }
            }
          },
          "fairplay_ksm_configured": {
            "type": "boolean"
          }
        }
      },
      "DRMAuthCode": {
        "type": "object",
        "description": "v3 — a named DRM auth code (the secret is never returned after creation)",
        "required": [
          "id",
          "name",
          "prefix",
          "scopes",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "prefix": {
            "type": "string",
            "description": "The 8 characters after vsdrm_"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "licence",
                "speke"
              ]
            }
          },
          "policy_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "DRMCount": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "total": {
            "type": "integer"
          },
          "ok": {
            "type": "integer"
          },
          "errors": {
            "type": "integer"
          }
        }
      },
      "DRMStats": {
        "type": "object",
        "description": "v3 — DRM statistics (the same counters as the usage meters)",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "timezone": {
            "type": "string"
          },
          "licences": {
            "type": "integer",
            "description": "Licences issued"
          },
          "keys": {
            "type": "integer",
            "description": "Content keys issued over SPEKE/CPIX"
          },
          "certificates": {
            "type": "integer",
            "description": "FairPlay certificate fetches"
          },
          "errors": {
            "type": "integer"
          },
          "by_system": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DRMCount"
            }
          },
          "by_request_type": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DRMCount"
            }
          },
          "by_status": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DRMCount"
            }
          },
          "by_platform": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DRMCount"
            }
          },
          "by_security_level": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DRMCount"
            }
          },
          "by_day": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DRMCount"
            }
          },
          "top_errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "system": {
                  "type": "string"
                },
                "platform": {
                  "type": "string"
                },
                "status": {
                  "type": "integer"
                },
                "error_code": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "alerts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "platform": {
                  "type": "string"
                },
                "hour": {
                  "type": "string",
                  "format": "date-time"
                },
                "requests": {
                  "type": "integer"
                },
                "errors": {
                  "type": "integer"
                },
                "error_rate": {
                  "type": "number"
                }
              }
            }
          }
        }
      },
      "FairPlayStatus": {
        "type": "object",
        "description": "v3 — FairPlay credentials status (no secrets)",
        "properties": {
          "configured": {
            "type": "boolean"
          },
          "subject": {
            "type": "string"
          },
          "not_after": {
            "type": "string",
            "format": "date-time"
          },
          "key_matches": {
            "type": "boolean"
          },
          "has_ask": {
            "type": "boolean"
          },
          "ksm_configured": {
            "type": "boolean"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AdRights": {
        "type": "object",
        "description": "v3 — the tenant's declaration that it holds the online rights to replace its broadcast's ad breaks",
        "required": [
          "online_rights",
          "note",
          "declared_by",
          "declared_at"
        ],
        "properties": {
          "online_rights": {
            "type": "boolean"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "declared_by": {
            "type": [
              "string",
              "null"
            ],
            "description": "`user:<email>`, `key:<prefix>` or `migration 0082` (grandfathered)"
          },
          "declared_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "VODAdPod": {
        "type": "object",
        "properties": {
          "max_ads": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10
          },
          "max_s": {
            "type": "integer",
            "minimum": 5,
            "maximum": 600
          }
        }
      },
      "VODAdPositions": {
        "type": "object",
        "description": "v3 — where library videos get their ads (fields not sent keep their default)",
        "properties": {
          "pre_roll": {
            "type": "boolean",
            "default": true
          },
          "mid_chapters": {
            "type": "boolean",
            "default": true,
            "description": "a mid-roll at each chapter start (metadata.chapters)"
          },
          "mid_cues": {
            "type": "boolean",
            "default": true,
            "description": "a mid-roll at each cue point (metadata.ads.cues)"
          },
          "mid_every_s": {
            "type": "integer",
            "default": 0,
            "description": "0 = none; 60..7200 — a mid-roll every N seconds where no chapter or cue break is"
          },
          "min_spacing_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3600,
            "default": 300
          },
          "guard_start_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3600,
            "default": 60,
            "description": "no mid-roll in the first N seconds"
          },
          "guard_end_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3600,
            "default": 60,
            "description": "no mid-roll in the last N seconds"
          },
          "post_roll": {
            "type": "boolean",
            "default": false
          },
          "pre_pod": {
            "$ref": "#/components/schemas/VODAdPod"
          },
          "mid_pod": {
            "$ref": "#/components/schemas/VODAdPod"
          },
          "post_pod": {
            "$ref": "#/components/schemas/VODAdPod"
          }
        }
      },
      "VODAdSettings": {
        "type": "object",
        "description": "v3 — the tenant's library ad settings",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "delivery": {
            "type": "string",
            "enum": [
              "ssai",
              "client"
            ]
          },
          "decisioning": {
            "type": "string",
            "enum": [
              "vast",
              "gam",
              "freewheel",
              "adocean"
            ]
          },
          "decisioning_params": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "vast_tag": {
            "type": [
              "string",
              "null"
            ]
          },
          "fallback_tag": {
            "type": [
              "string",
              "null"
            ]
          },
          "vast_timeout_ms": {
            "type": "integer"
          },
          "freq_cap_per_hour": {
            "type": [
              "integer",
              "null"
            ]
          },
          "contextual_kv": {
            "type": "boolean"
          },
          "positions": {
            "$ref": "#/components/schemas/VODAdPositions"
          },
          "updated_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "online_rights": {
            "type": "boolean",
            "description": "the tenant declared its ad rights (/v1/ads/rights); without them no library ad plays"
          },
          "stitching": {
            "type": "boolean",
            "description": "the platform runs server-side ads; without it the /m/ssai/vod routes serve the clean video"
          }
        }
      },
      "VODAdOverrideInput": {
        "type": "object",
        "required": [
          "mode"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "custom",
              "off"
            ]
          },
          "positions": {
            "$ref": "#/components/schemas/VODAdPositions"
          }
        }
      },
      "VODAdOverride": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "subject_type": {
            "type": "string",
            "enum": [
              "asset",
              "collection"
            ]
          },
          "subject_id": {
            "type": "string",
            "format": "uuid"
          },
          "mode": {
            "type": "string",
            "enum": [
              "custom",
              "off"
            ]
          },
          "positions": {
            "$ref": "#/components/schemas/VODAdPositions"
          },
          "updated_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VODAdSlot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "pre | post | mid-<planned ms>"
          },
          "kind": {
            "type": "string",
            "enum": [
              "pre",
              "mid",
              "post"
            ]
          },
          "at_ms": {
            "type": "integer",
            "description": "on the content timeline, snapped to a segment boundary"
          },
          "source": {
            "type": "string",
            "enum": [
              "start",
              "chapter",
              "cue",
              "interval",
              "end"
            ]
          },
          "max_ads": {
            "type": "integer"
          },
          "max_ms": {
            "type": "integer"
          }
        }
      },
      "AssetAdPositions": {
        "type": "object",
        "description": "v3 — a video's ad positions and planned breaks",
        "properties": {
          "override": {
            "$ref": "#/components/schemas/VODAdOverride"
          },
          "effective": {
            "$ref": "#/components/schemas/VODAdPositions"
          },
          "from": {
            "type": "string",
            "enum": [
              "asset",
              "collection",
              "tenant"
            ]
          },
          "off": {
            "type": "boolean"
          },
          "enabled": {
            "type": "boolean"
          },
          "delivery": {
            "type": "string",
            "enum": [
              "ssai",
              "client"
            ]
          },
          "duration_ms": {
            "type": "integer"
          },
          "chapters": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "start_ms": {
                  "type": "integer"
                },
                "title": {
                  "type": "string"
                }
              }
            }
          },
          "cues_ms": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VODAdSlot"
            }
          },
          "blocked": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "disabled",
                "no_rights",
                "no_ad_server",
                "not_ready",
                "off",
                "platform_off"
              ]
            }
          },
          "ssai_url": {
            "type": "string"
          },
          "vmap_url": {
            "type": "string"
          }
        }
      },
      "AdDecision": {
        "type": "object",
        "description": "v3 — one (viewer session, break) server-side ad decision as stitched",
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "channel": {
            "type": "string",
            "description": "channel slug"
          },
          "break_id": {
            "type": "string",
            "format": "uuid"
          },
          "session_id": {
            "type": "string",
            "description": "the player's random playback session id ('' = none)"
          },
          "decided_at": {
            "type": "string",
            "format": "date-time"
          },
          "adapter": {
            "type": "string",
            "enum": [
              "vast",
              "gam",
              "freewheel",
              "adocean"
            ]
          },
          "outcome": {
            "type": "string",
            "enum": [
              "filled",
              "no_fill",
              "error",
              "timeout",
              "capped"
            ]
          },
          "error_code": {
            "type": "string",
            "description": "VAST error code (301 failed request, 303 no fill, 100 parse, …); omitted when none"
          },
          "latency_ms": {
            "type": "integer"
          },
          "used_fallback": {
            "type": "boolean"
          },
          "ads_returned": {
            "type": "integer"
          },
          "ads_stitched": {
            "type": "integer",
            "description": "ads whose creative was ready for the channel ladder and fitted the break"
          },
          "stitched_ms": {
            "type": "integer"
          },
          "ads": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "ad_id": {
                  "type": "string"
                },
                "creative_id": {
                  "type": "string"
                },
                "ad_system": {
                  "type": "string"
                },
                "duration_ms": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "AdReconciliationRow": {
        "type": "object",
        "properties": {
          "channel": {
            "type": "string"
          },
          "decisions": {
            "type": "integer"
          },
          "filled": {
            "type": "integer"
          },
          "no_fill": {
            "type": "integer"
          },
          "errors": {
            "type": "integer"
          },
          "timeouts": {
            "type": "integer"
          },
          "capped": {
            "type": "integer"
          },
          "fallbacks": {
            "type": "integer"
          },
          "ads_stitched": {
            "type": "integer"
          },
          "stitched_seconds": {
            "type": "integer"
          }
        }
      },
      "AdBreak": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "start_at": {
            "type": "string",
            "format": "date-time",
            "description": "on the recording timeline (PROGRAM-DATE-TIME)"
          },
          "duration_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null = open (cue-out seen, cue-in not yet)"
          },
          "end_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "source": {
            "type": "string",
            "enum": [
              "scte35",
              "manual",
              "detected",
              "redge_dai"
            ]
          },
          "source_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "the cue source's id (the Redge period id)"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AdBreakInput": {
        "type": "object",
        "properties": {
          "start_at": {
            "type": "string",
            "format": "date-time",
            "description": "required on create; within the last 120 days or the next 24 h"
          },
          "duration_ms": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 1800000,
            "description": "required on create"
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "\"\" clears it"
          }
        }
      },
      "ChannelRecording": {
        "type": "object",
        "description": "A channel's recording mode. A channel never configured is `single` at revision 0.",
        "required": [
          "mode",
          "legs",
          "revision",
          "warnings",
          "storage_bytes_per_day",
          "single_storage_bytes_per_day",
          "dual_modes_enabled",
          "encoders"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "single",
              "dual_same_source",
              "dual_split_source"
            ],
            "description": "single = one recorder; dual_same_source = two recorders of the same feed; dual_split_source = leg 1 records feed A, leg 2 feed B"
          },
          "legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecordingLeg"
            },
            "description": "empty in single mode"
          },
          "revision": {
            "type": "integer",
            "description": "send it back as `revision` on PUT to detect concurrent edits"
          },
          "updated_by": {
            "type": "string",
            "description": "`user:<email>` or `key:<prefix>`; omitted before the first change"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "omitted before the first change"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "set by PUT (e.g. both legs on one host); always empty on GET"
          },
          "storage_bytes_per_day": {
            "type": "integer",
            "description": "estimated recorded bytes per day in this mode (ladder bitrates × 86400 s, both legs counted)"
          },
          "dual_modes_enabled": {
            "type": "boolean",
            "description": "the platform allows the dual modes (RECORDING_REDUNDANCY)"
          },
          "single_storage_bytes_per_day": {
            "type": "integer",
            "description": "the one-recorder estimate (to compare before switching)"
          },
          "encoders": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "recorder host names a leg can name (seen recording or assigned in the last day, plus this channel's own)"
          }
        }
      },
      "RecordingLeg": {
        "type": "object",
        "required": [
          "leg",
          "encoder"
        ],
        "properties": {
          "leg": {
            "type": "integer",
            "enum": [
              1,
              2
            ]
          },
          "encoder": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9.-]{0,62}$",
            "description": "recorder (encoder) host name"
          }
        }
      },
      "RecordingRevision": {
        "type": "object",
        "properties": {
          "revision": {
            "type": "integer"
          },
          "mode": {
            "type": "string",
            "enum": [
              "single",
              "dual_same_source",
              "dual_split_source"
            ]
          },
          "legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecordingLeg"
            }
          },
          "updated_by": {
            "type": "string",
            "description": "omitted when unknown"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecordingLegStatus": {
        "type": "object",
        "properties": {
          "leg": {
            "type": "integer"
          },
          "encoder": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "recording",
              "behind",
              "down"
            ],
            "description": "from the leg's newest indexed segment: older than 60 s = behind, older than 180 s (or none in 10 min) = down"
          },
          "last_segment_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "RecordingStatus": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "single",
              "dual_same_source",
              "dual_split_source"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "redundancy_lost",
              "down"
            ],
            "description": "ok = every leg recording; redundancy_lost = a dual channel with one leg recording; down = no leg recording (a single channel is ok or down)"
          },
          "serving_leg": {
            "type": "integer",
            "description": "the first leg that is recording (0 = none)"
          },
          "legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecordingLegStatus"
            }
          }
        }
      },
      "RecordingStatusChange": {
        "type": "object",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "mode": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "redundancy_lost",
              "down"
            ]
          },
          "serving_leg": {
            "type": "integer"
          },
          "legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecordingLegStatus"
            }
          }
        }
      },
      "ChannelIngest": {
        "type": "object",
        "description": "Studio ingest settings of a channel. Secrets (pull URLs with tokens, SRT passphrases, RTMP stream keys) are never included — only `latest.secrets_set` / `applied.secrets_set`. A channel that never saved settings answers `configured: false`, `apply_state: draft` and empty lists.",
        "required": [
          "channel_id",
          "encoder",
          "configured",
          "managed",
          "desired_rev",
          "applied_rev",
          "apply_state",
          "control",
          "endpoints",
          "status",
          "status_stale",
          "allowed_ips",
          "source_exceptions",
          "probes",
          "legacy",
          "trusted_auto_approve",
          "limits"
        ],
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "encoder": {
            "type": "string",
            "description": "encoder host that runs the channel's ingest (the channel's primary encoder)"
          },
          "configured": {
            "type": "boolean",
            "description": "settings were saved at least once"
          },
          "managed": {
            "type": "boolean",
            "description": "the encoder agent runs this channel's feed"
          },
          "desired_rev": {
            "type": "integer",
            "description": "the revision the agent is asked to run (0 = none)"
          },
          "applied_rev": {
            "type": "integer",
            "description": "the revision the agent reported running (0 = none)"
          },
          "apply_state": {
            "type": "string",
            "enum": [
              "draft",
              "validated",
              "applying",
              "applied",
              "failed",
              "rolled_back",
              "conflict",
              "disabled"
            ]
          },
          "apply_error": {
            "type": [
              "string",
              "null"
            ],
            "description": "why the last apply failed, rolled back or conflicted"
          },
          "apply_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when the current apply was requested"
          },
          "control": {
            "type": "string",
            "enum": [
              "auto",
              "a",
              "b",
              "slate"
            ],
            "description": "operator command: automatic failover, a pinned feed, or the slate"
          },
          "latest": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/IngestRevision"
              },
              {
                "type": "null"
              }
            ],
            "description": "the newest saved revision (the draft)"
          },
          "applied": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/IngestRevision"
              },
              {
                "type": "null"
              }
            ],
            "description": "the running revision"
          },
          "endpoints": {
            "type": "object",
            "description": "push feeds of the latest revision that have a port, by feed (`a`, `b`) — where the encoder sends to; no secrets",
            "additionalProperties": {
              "$ref": "#/components/schemas/IngestEndpoint"
            }
          },
          "status": {
            "$ref": "#/components/schemas/IngestAgentStatus"
          },
          "status_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when the agent last reported"
          },
          "status_stale": {
            "type": "boolean",
            "description": "managed and no report for 30 s (status may be out of date)"
          },
          "allowed_ips": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IngestAllowedIP"
            },
            "description": "pending and approved encoder addresses, plus ones decided in the last 7 days (at most 100)"
          },
          "source_exceptions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "`<feed>:<host>` pull sources an Interhost operator allowed although they are not public"
          },
          "probes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IngestProbe"
            },
            "description": "connectivity checks of the latest revision"
          },
          "legacy": {
            "type": "object",
            "description": "the shared encoder endpoints used before Studio ingest",
            "properties": {
              "srt": {
                "type": "string",
                "example": "srt://ingest-poc.vustream.net:9999"
              },
              "rtmp": {
                "type": "string",
                "example": "rtmp://ingest-poc.vustream.net:1935"
              }
            }
          },
          "trusted_auto_approve": {
            "type": "boolean",
            "description": "an operator marked the tenant trusted: push sources apply at once"
          },
          "legacy_relay": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/IngestLegacyRelay"
              },
              {
                "type": "null"
              }
            ],
            "description": "the operator relay the channel runs today (source shown as scheme://host/path only)"
          },
          "migration": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/IngestMigration"
              },
              {
                "type": "null"
              }
            ],
            "description": "the latest migration request to Studio ingest"
          },
          "limits": {
            "type": "object",
            "properties": {
              "max_managed_per_encoder": {
                "type": "integer",
                "example": 2
              },
              "port_min": {
                "type": "integer",
                "example": 10100
              },
              "port_max": {
                "type": "integer",
                "example": 10299
              },
              "failover_after_s": {
                "type": "array",
                "items": {
                  "type": "integer"
                },
                "example": [
                  2,
                  60
                ],
                "description": "allowed range [min, max]"
              },
              "return_after_s": {
                "type": "array",
                "items": {
                  "type": "integer"
                },
                "example": [
                  5,
                  3600
                ],
                "description": "allowed range [min, max]"
              }
            }
          }
        }
      },
      "IngestRevision": {
        "type": "object",
        "description": "One saved ingest revision (no secrets).",
        "required": [
          "rev",
          "config",
          "secrets_set",
          "created_by",
          "created_at"
        ],
        "properties": {
          "rev": {
            "type": "integer"
          },
          "config": {
            "$ref": "#/components/schemas/IngestConfig"
          },
          "secrets_set": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            },
            "description": "which sealed secrets the revision holds, keyed `<feed>.<name>` (e.g. `a.url`, `b.passphrase`, `a.stream_key`)"
          },
          "validation": {
            "$ref": "#/components/schemas/IngestValidation"
          },
          "created_by": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IngestConfig": {
        "type": "object",
        "required": [
          "feeds",
          "failover"
        ],
        "properties": {
          "feeds": {
            "type": "object",
            "description": "feed `a` (primary, always) and `b` (backup, optional)",
            "additionalProperties": {
              "$ref": "#/components/schemas/IngestFeedConfig"
            }
          },
          "failover": {
            "$ref": "#/components/schemas/IngestFailover"
          }
        }
      },
      "IngestFeedConfig": {
        "type": "object",
        "description": "One feed as stored (no secrets).",
        "required": [
          "mode"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "push_srt",
              "pull_srt",
              "pull_hls",
              "pull_rtmp",
              "push_rtmp"
            ]
          },
          "url_display": {
            "type": "string",
            "description": "pull_hls / pull_rtmp: scheme://host[:port]/path; a query string shows as `?…`"
          },
          "host": {
            "type": "string",
            "description": "pull_srt host, or the pull URL's host"
          },
          "port": {
            "type": "integer",
            "description": "pull_srt remote port"
          },
          "stream_id": {
            "type": "string",
            "description": "push_srt / pull_srt stream id (push_srt defaults to `<tenant>/<channel slug>/<feed>`)"
          },
          "latency_ms": {
            "type": "integer",
            "description": "push_srt / pull_srt latency (default 200)"
          }
        }
      },
      "IngestFeedInput": {
        "type": "object",
        "description": "One feed in PUT /v1/channels/{id}/ingest. Secret fields (`url`, `passphrase`, `stream_key`): omit to keep the stored value when the feed keeps its mode; push_srt passphrases and push_rtmp stream keys are generated when none is stored; `regenerate: true` forces a new generated secret.",
        "required": [
          "mode"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "push_srt",
              "pull_srt",
              "pull_hls",
              "pull_rtmp",
              "push_rtmp"
            ]
          },
          "url": {
            "type": "string",
            "description": "pull_hls: https only, port 443 or 8443; pull_rtmp: rtmp:// or rtmps://, port 1935 or 443. No credentials in the URL; the host must resolve to a public address"
          },
          "host": {
            "type": "string",
            "description": "pull_srt: host name or IP address only (public)"
          },
          "port": {
            "type": "integer",
            "minimum": 1024,
            "maximum": 65535,
            "description": "pull_srt remote port"
          },
          "stream_id": {
            "type": "string",
            "maxLength": 256,
            "description": "push_srt / pull_srt; 1–256 printable characters, no spaces"
          },
          "latency_ms": {
            "type": "integer",
            "minimum": 20,
            "maximum": 8000,
            "default": 200,
            "description": "push_srt / pull_srt"
          },
          "passphrase": {
            "type": "string",
            "minLength": 10,
            "maxLength": 79,
            "description": "push_srt / pull_srt (SRT encryption)"
          },
          "stream_key": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{8,128}$",
            "description": "push_rtmp"
          },
          "regenerate": {
            "type": "boolean",
            "description": "generate a new passphrase / stream key"
          }
        }
      },
      "IngestFailover": {
        "type": "object",
        "description": "Failover policy between feed A and feed B. Zero or missing numbers take the defaults.",
        "properties": {
          "auto": {
            "type": "boolean",
            "description": "switch to B when A is lost for `after_s`"
          },
          "after_s": {
            "type": "integer",
            "minimum": 2,
            "maximum": 60,
            "default": 5
          },
          "return": {
            "type": "string",
            "enum": [
              "auto",
              "manual"
            ],
            "default": "auto",
            "description": "auto = back to A once it is stable for `return_after_s`"
          },
          "return_after_s": {
            "type": "integer",
            "minimum": 5,
            "maximum": 3600,
            "default": 30
          },
          "slate": {
            "type": "boolean",
            "description": "play the slate when no feed is up"
          },
          "min_hold_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3600,
            "default": 60,
            "description": "stay on B at least this long after an automatic switch"
          },
          "max_switches_per_hour": {
            "type": "integer",
            "minimum": 1,
            "maximum": 60,
            "default": 6,
            "description": "automatic switches per hour (a lost active feed still switches)"
          },
          "triggers": {
            "type": "object",
            "description": "optional quality triggers on the active feed, 0 = off (default); a trigger switches only when the other feed is healthy",
            "properties": {
              "frozen_s": {
                "type": "integer",
                "description": "picture unchanged ≥ N s; 0 or 3..600"
              },
              "silence_s": {
                "type": "integer",
                "description": "audio below -50 dB ≥ N s; 0 or 3..600"
              },
              "black_s": {
                "type": "integer",
                "description": "black picture ≥ N s; 0 or 3..600"
              },
              "decode_errors_per_min": {
                "type": "integer",
                "minimum": 0,
                "maximum": 10000
              },
              "bitrate_drop_pct": {
                "type": "integer",
                "minimum": 0,
                "maximum": 90,
                "description": "bitrate below N % of its last-minute median"
              }
            }
          }
        }
      },
      "IngestEndpoint": {
        "type": "object",
        "description": "Where an encoder pushes a push feed (no passphrase or stream key).",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "push_srt",
              "push_rtmp"
            ]
          },
          "url": {
            "type": "string",
            "description": "srt://host:port?streamid=…&latency=<µs> or rtmp://host:port/live"
          },
          "host": {
            "type": "string"
          },
          "port": {
            "type": "integer"
          },
          "proto": {
            "type": "string",
            "enum": [
              "udp",
              "tcp"
            ]
          },
          "stream_id": {
            "type": "string"
          }
        }
      },
      "IngestAgentStatus": {
        "type": "object",
        "description": "The encoder agent's last report (`{}` before the first one).",
        "properties": {
          "active": {
            "type": "string",
            "enum": [
              "a",
              "b",
              "slate",
              "none"
            ],
            "description": "what the publisher sends now"
          },
          "running_rev": {
            "type": "integer"
          },
          "feeds": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/IngestFeedStatus"
            }
          },
          "extra": {
            "type": "object",
            "additionalProperties": true,
            "description": "agent diagnostics (control, applying, firewall, playlist_fresh, playlist_age_s, source_switches, publisher_restarts, …)"
          }
        }
      },
      "IngestFeedStatus": {
        "type": "object",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "up",
              "down",
              "waiting",
              "starting"
            ],
            "description": "waiting = a push feed that never connected"
          },
          "since": {
            "type": "string",
            "format": "date-time"
          },
          "bytes_per_s": {
            "type": "integer"
          },
          "restarts": {
            "type": "integer"
          },
          "last_error": {
            "type": "string",
            "description": "redacted"
          },
          "listen_port": {
            "type": "integer"
          },
          "health": {
            "type": "string",
            "enum": [
              "ok",
              "frozen",
              "silence",
              "black",
              "decode_errors",
              "bitrate_drop"
            ],
            "description": "only with a quality trigger on"
          },
          "health_detail": {
            "type": "string"
          }
        }
      },
      "IngestAllowedIP": {
        "type": "object",
        "description": "An encoder source address of a push feed. `state` is what Studio shows: pending, active, rejected, expired or revoked.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "feed": {
            "type": "string",
            "enum": [
              "a",
              "b"
            ]
          },
          "cidr": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "revoked"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "rejected",
              "expired",
              "revoked"
            ]
          },
          "requested_by": {
            "type": "string"
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "decided_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "decided_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "decision_note": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "IngestProbe": {
        "type": "object",
        "description": "A connectivity check of a pull feed, run by the encoder agent with ffprobe (no ingest).",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "rev": {
            "type": "integer"
          },
          "feed": {
            "type": "string",
            "enum": [
              "a",
              "b"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "ok",
              "failed"
            ],
            "description": "failed also when the agent does not answer within 90 s"
          },
          "result": {
            "type": "object",
            "additionalProperties": true,
            "description": "ffprobe summary (video, audio, tls, streams) or `error`"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IngestValidation": {
        "type": "object",
        "description": "Result of POST …/ingest/validate. `ok` is null while connectivity checks run, true when there are no errors and no checks, false with errors; apply re-evaluates the checks.",
        "properties": {
          "ok": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "rev": {
            "type": "integer"
          },
          "plan": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "the agent's processes for this revision (`[role] argv`, secrets shown as ***)"
          },
          "diff": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "`- ` / `+ ` lines against the running revision's config and plan"
          },
          "probes": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "connectivity checks queued for pull feeds (see `probes` in GET …/ingest)"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "diff_against_rev": {
            "type": "integer",
            "description": "the running revision the diff compares with (0 = none)"
          }
        }
      },
      "IngestLegacyRelay": {
        "type": "object",
        "properties": {
          "instance": {
            "type": "string",
            "description": "`<tenant>_<channel slug>`"
          },
          "url_display": {
            "type": "string",
            "description": "scheme://host/path of the relay source (query hidden)"
          },
          "unit_active": {
            "type": "boolean"
          },
          "unit_enabled": {
            "type": "boolean"
          },
          "seen_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IngestMigration": {
        "type": "object",
        "description": "A request to move the channel from the operator relay to Studio ingest; an Interhost operator approves and cuts over.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "rev": {
            "type": "integer",
            "description": "the draft revision the move creates"
          },
          "instance": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "requested",
              "approved",
              "cutting_over",
              "done",
              "rolled_back",
              "failed",
              "cancelled"
            ]
          },
          "checklist": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "object",
              "properties": {
                "at": {
                  "type": "string",
                  "format": "date-time"
                },
                "by": {
                  "type": "string"
                }
              }
            },
            "description": "operator checklist items ticked (window_announced, backup_or_waived, rollback_understood)"
          },
          "requested_by": {
            "type": "string"
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "approved_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "approved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "IngestEvent": {
        "type": "object",
        "description": "One entry of the channel's ingest history.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "kind": {
            "type": "string",
            "description": "e.g. failover, return, switch, slate_on, slate_off, failover_blocked (agent); apply_requested, applied, rolled_back, conflict, disabled, switch_requested, auto_requested, slate_requested, resume_requested, restart_requested, ip_requested, ip_approved, ip_revoked, migration_requested, migration_approved, migration_cutting_over, migration_done"
          },
          "from": {
            "type": [
              "string",
              "null"
            ],
            "description": "feed / state before (a, b, slate, none)"
          },
          "to": {
            "type": [
              "string",
              "null"
            ]
          },
          "actor": {
            "type": "string",
            "description": "user e-mail or `API key <name>` for requests; `agent:<encoder>` for the encoder agent"
          },
          "detail": {
            "type": "object",
            "additionalProperties": true,
            "description": "kind-specific (rev, code, reason, cidr, seq, …)"
          }
        }
      },
      "IngestControlResult": {
        "type": "object",
        "required": [
          "control",
          "control_seq"
        ],
        "properties": {
          "control": {
            "type": "string",
            "enum": [
              "auto",
              "a",
              "b",
              "slate"
            ]
          },
          "control_seq": {
            "type": "integer",
            "description": "sequence of the stored command; the agent acts on a higher sequence than it saw"
          }
        }
      },
      "OwnCDN": {
        "type": "object",
        "description": "A tenant's own CDN as a steering pathway. Secrets are never returned.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "pathway_id": {
            "type": "string",
            "example": "akamai"
          },
          "name": {
            "type": "string"
          },
          "hostname": {
            "type": "string",
            "example": "tv10.akamaized.net"
          },
          "token_scheme": {
            "type": "string",
            "enum": [
              "none",
              "akamai_edgeauth",
              "cdn77_md5path",
              "bunny_sha256",
              "bunny_hs256",
              "nginx_secure_link"
            ]
          },
          "token_param": {
            "type": "string"
          },
          "token_secret_set": {
            "type": "boolean"
          },
          "purge_kind": {
            "type": "string",
            "enum": [
              "none",
              "http",
              "cdn77",
              "bunny"
            ]
          },
          "purge_config": {
            "type": "object",
            "additionalProperties": true
          },
          "purge_credentials_set": {
            "type": "boolean"
          },
          "cost_per_tb": {
            "type": [
              "number",
              "null"
            ]
          },
          "cost_currency": {
            "type": "string"
          },
          "legacy": {
            "type": "boolean",
            "description": "the tenant's existing CDN in a side-by-side migration"
          },
          "enabled": {
            "type": "boolean"
          },
          "eligible": {
            "type": "boolean"
          },
          "reason": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OwnCDNInput": {
        "type": "object",
        "properties": {
          "pathway_id": {
            "type": "string",
            "description": "create only: 2–24 of a-z 0-9 -, not il"
          },
          "name": {
            "type": "string"
          },
          "hostname": {
            "type": "string"
          },
          "token_scheme": {
            "type": "string",
            "enum": [
              "none",
              "akamai_edgeauth",
              "cdn77_md5path",
              "bunny_sha256",
              "bunny_hs256",
              "nginx_secure_link"
            ]
          },
          "token_secret": {
            "type": "string",
            "writeOnly": true
          },
          "token_param": {
            "type": "string"
          },
          "purge_kind": {
            "type": "string",
            "enum": [
              "none",
              "http",
              "cdn77",
              "bunny"
            ]
          },
          "purge_config": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string"
              },
              "method": {
                "type": "string",
                "enum": [
                  "POST",
                  "PUT"
                ]
              },
              "header": {
                "type": "string"
              },
              "resource_id": {
                "type": "string"
              },
              "zone_id": {
                "type": "string"
              }
            }
          },
          "purge_credentials": {
            "type": "string",
            "writeOnly": true
          },
          "cost_per_tb": {
            "type": [
              "number",
              "null"
            ]
          },
          "cost_currency": {
            "type": "string"
          },
          "legacy": {
            "type": "boolean"
          },
          "enabled": {
            "type": "boolean"
          }
        }
      },
      "DistributionEdgeOptions": {
        "type": "object",
        "description": "Migration helpers of a CDN-only distribution. Nothing is set by default.",
        "properties": {
          "distribution_id": {
            "type": "string",
            "format": "uuid"
          },
          "token_adapter": {
            "type": "string",
            "enum": [
              "none",
              "akamai_edgeauth",
              "cdn77_md5path",
              "bunny_sha256",
              "bunny_hs256",
              "nginx_secure_link"
            ]
          },
          "token_adapter_param": {
            "type": "string"
          },
          "token_adapter_secret_set": {
            "type": "boolean"
          },
          "rewrite_hosts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rewrite_active": {
            "type": "boolean"
          },
          "rewrite_from": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "carry_token": {
            "type": "boolean"
          },
          "selector_mode": {
            "type": "boolean"
          },
          "selector_label": {
            "type": "string"
          },
          "selector_pathway_label": {
            "type": "string"
          },
          "token_schemes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "cdn_only": {
            "type": "boolean"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CDNPartner": {
        "type": "object",
        "description": "A partner CDN account (credentials, shield and token secrets are never returned)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "provider": {
            "type": "string",
            "enum": [
              "bunny",
              "cdn77"
            ]
          },
          "name": {
            "type": "string"
          },
          "pathway_id": {
            "type": "string",
            "description": "Steering pathway id of this partner"
          },
          "credentials_updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "token_auth": {
            "type": "boolean",
            "description": "The zone enforces token authentication (some playback policy requires tokens)"
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "connecting",
              "connected",
              "error",
              "disconnected"
            ]
          },
          "status_detail": {
            "type": "string"
          },
          "health": {
            "type": "string",
            "enum": [
              "ok",
              "failing",
              "unknown"
            ]
          },
          "health_detail": {
            "type": "string"
          },
          "health_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "shield_ready": {
            "type": "boolean",
            "description": "The tenant shield hostname answers on the edge"
          },
          "enabled": {
            "type": "boolean"
          },
          "connected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "disconnected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "zone": {
            "type": [
              "object",
              "null"
            ],
            "description": "The pull zone at the provider (null until connected)",
            "properties": {
              "id": {
                "type": "string"
              },
              "host": {
                "type": "string"
              },
              "created_by_us": {
                "type": "boolean",
                "description": "ViewStream created it (only such a zone can be deleted through the API)"
              }
            }
          },
          "shield_host": {
            "type": "string",
            "description": "`<tenant>.<shield domain>` — the origin the zone pulls from"
          },
          "killed": {
            "type": "boolean",
            "description": "Interhost switched this provider/pathway off for the tenant"
          },
          "kill_reason": {
            "type": "string"
          },
          "usage_24h": {
            "type": "object",
            "properties": {
              "bytes": {
                "type": "integer",
                "format": "int64"
              },
              "source": {
                "type": "string",
                "enum": [
                  "provider",
                  "none"
                ]
              }
            }
          }
        }
      },
      "CDNPartnerProvider": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "bunny",
              "cdn77"
            ]
          },
          "name": {
            "type": "string"
          },
          "credential_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Fields of `credentials` (`api_key` / `api_token`)"
          }
        }
      },
      "SteeringGeoRule": {
        "type": "object",
        "required": [
          "countries",
          "pathways"
        ],
        "properties": {
          "countries": {
            "type": "array",
            "minItems": 1,
            "maxItems": 250,
            "items": {
              "type": "string"
            },
            "description": "ISO 3166-1 alpha-2 codes, or `*` for any"
          },
          "except": {
            "type": "array",
            "maxItems": 250,
            "items": {
              "type": "string"
            },
            "description": "Countries excluded from the match"
          },
          "pathways": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "Pathway priority for matching viewers (`il` or partner ids)"
          }
        }
      },
      "SteeringLoadRule": {
        "type": "object",
        "description": "Overflow load controller — above the threshold it ramps a share of new sessions to partner pathways",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "scope": {
            "type": "string",
            "enum": [
              "tenant",
              "overall"
            ],
            "default": "tenant"
          },
          "metric": {
            "type": "string",
            "enum": [
              "gbps",
              "pct"
            ],
            "default": "gbps"
          },
          "threshold": {
            "type": "number",
            "description": "Gbps (> 0, ≤ 10000) or percent of capacity (1–100)"
          },
          "hysteresis_pct": {
            "type": "number",
            "minimum": 0,
            "maximum": 90,
            "default": 20
          },
          "share_max": {
            "type": "number",
            "minimum": 1,
            "maximum": 100,
            "default": 30
          },
          "ramp_step": {
            "type": "number",
            "minimum": 1,
            "maximum": 100,
            "default": 5
          },
          "ramp_every_s": {
            "type": "integer",
            "minimum": 15,
            "maximum": 3600,
            "default": 60
          },
          "min_hold_s": {
            "type": "integer",
            "minimum": 0,
            "maximum": 86400,
            "default": 300
          },
          "pathways": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Partner pathways that receive the overflow"
          }
        }
      },
      "SteeringOverride": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "arm_overflow",
              "force"
            ]
          },
          "pathway": {
            "type": [
              "string",
              "null"
            ]
          },
          "share": {
            "type": [
              "number",
              "null"
            ]
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "note": {
            "type": "string"
          },
          "cancelled_at": {
            "type": "string",
            "format": "date-time",
            "description": "Present once cancelled"
          },
          "created_by": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "state": {
            "type": "string",
            "enum": [
              "scheduled",
              "active",
              "ended",
              "cancelled"
            ]
          }
        }
      },
      "SteeringGuard": {
        "type": "object",
        "description": "A scope's guard rails: at most `max_partner_share` % of sessions (by bucket) on a partner pathway (0 = no cap)",
        "properties": {
          "max_partner_share": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          }
        }
      },
      "SteeringRule": {
        "type": "object",
        "description": "One ordered rule (v3 cdn-steering). `when` combines with AND: `countries`, `asns`, `cidrs` take `{in: [...]}` or\n`{not: [...]}` (an unknown country or ASN matches only `not`); `paths` are globs (`*` = anything, any of them);\n`headers` match by name (any value, or a glob); `window` is weekly (`days` 1=Mon…7=Sun, `from`/`to` HH:MM in `tz`,\ndefault Asia/Jerusalem; `to` < `from` crosses midnight) or absolute (`start`/`end`); `health`, `traffic` (Gbps) and\n`qoe` (smoothness 0–100) are live conditions the publisher resolves every 15 s — unknown = false. `then` routes to a\npathway or a weighted group (weights 1–100, deterministic per session) and else the first eligible `fallback`, else `il`.",
        "required": [
          "id",
          "enabled",
          "when",
          "then"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9_-]{0,31}$"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "enabled": {
            "type": "boolean"
          },
          "when": {
            "type": "object",
            "properties": {
              "countries": {
                "type": "object",
                "properties": {
                  "in": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "not": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "asns": {
                "type": "object",
                "properties": {
                  "in": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "not": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              },
              "cidrs": {
                "type": "object",
                "properties": {
                  "in": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "not": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "paths": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "headers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "name"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    }
                  }
                }
              },
              "window": {
                "type": "object",
                "properties": {
                  "days": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 7
                    }
                  },
                  "from": {
                    "type": "string"
                  },
                  "to": {
                    "type": "string"
                  },
                  "tz": {
                    "type": "string"
                  },
                  "start": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "end": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              "health": {
                "type": "object",
                "properties": {
                  "pathway": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string",
                    "enum": [
                      "ok",
                      "down"
                    ]
                  }
                }
              },
              "traffic": {
                "type": "object",
                "properties": {
                  "pathway": {
                    "type": "string"
                  },
                  "op": {
                    "type": "string",
                    "enum": [
                      ">",
                      ">=",
                      "<",
                      "<="
                    ]
                  },
                  "gbps": {
                    "type": "number"
                  }
                }
              },
              "qoe": {
                "type": "object",
                "properties": {
                  "pathway": {
                    "type": "string"
                  },
                  "asn": {
                    "type": "integer"
                  },
                  "op": {
                    "type": "string",
                    "enum": [
                      ">",
                      ">=",
                      "<",
                      "<="
                    ]
                  },
                  "score": {
                    "type": "number"
                  }
                }
              }
            }
          },
          "then": {
            "type": "object",
            "required": [
              "pathways"
            ],
            "properties": {
              "pathways": {
                "type": "array",
                "minItems": 1,
                "maxItems": 8,
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "weight"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "weight": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 100
                    }
                  }
                }
              },
              "fallback": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "SteeringRuleSet": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "distribution_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "host": {
            "type": "string",
            "description": "the distribution's hostname ('' = the tenant's scope)"
          },
          "version": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published",
              "archived"
            ]
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SteeringRule"
            }
          },
          "guard": {
            "$ref": "#/components/schemas/SteeringGuard"
          },
          "note": {
            "type": "string"
          },
          "created_by": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "published_by": {
            "type": "string"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SteeringManifest": {
        "type": "object",
        "description": "HLS content-steering manifest (RFC 8216bis)",
        "properties": {
          "VERSION": {
            "type": "integer"
          },
          "TTL": {
            "type": "integer",
            "description": "Seconds until the player reloads"
          },
          "RELOAD-URI": {
            "type": "string"
          },
          "PATHWAY-PRIORITY": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "PATHWAY-CLONES": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "BASE-ID": {
                  "type": "string"
                },
                "ID": {
                  "type": "string"
                },
                "URI-REPLACEMENT": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "SteeringView": {
        "type": "object",
        "properties": {
          "config": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "ttl_s": {
                "type": "integer"
              },
              "geo_rules": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SteeringGeoRule"
                }
              },
              "load_rule": {
                "$ref": "#/components/schemas/SteeringLoadRule"
              },
              "weights": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "updated_by": {
                "type": "string"
              },
              "updated_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "state": {
            "type": "object",
            "properties": {
              "active": {
                "type": "boolean",
                "description": "A steering document is published for the tenant"
              },
              "share": {
                "type": "number",
                "description": "Current overflow share (percent)"
              },
              "phase": {
                "type": "string",
                "description": "Load-controller phase (`idle` when it never ran)"
              },
              "since": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "measured": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Last measured load in `measured_unit`"
              },
              "measured_unit": {
                "type": "string",
                "enum": [
                  "gbps",
                  "pct"
                ]
              },
              "capacity_gbps": {
                "type": "number"
              },
              "load_error": {
                "type": "string"
              }
            }
          },
          "pathways": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "interhost",
                    "partner"
                  ]
                },
                "provider": {
                  "type": "string"
                },
                "host": {
                  "type": "string"
                },
                "eligible": {
                  "type": "boolean"
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "",
                    "disabled",
                    "not_connected",
                    "unhealthy",
                    "killed",
                    "needs_token_auth",
                    "drm"
                  ]
                }
              }
            }
          },
          "overrides": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SteeringOverride"
            },
            "description": "Overrides of the last 7 days"
          },
          "doc": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "serial": {
                "type": "integer"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "ttl": {
                "type": "integer"
              }
            }
          },
          "steer_url": {
            "type": "string"
          }
        }
      },
      "DeliveryProbeStep": {
        "type": "object",
        "description": "One probe of the playback checker",
        "properties": {
          "step": {
            "type": "string",
            "enum": [
              "master",
              "media",
              "segment"
            ]
          },
          "url": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "ms": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "content_type": {
            "type": "string"
          },
          "cache": {
            "type": "string",
            "description": "X-Cache of the edge"
          },
          "cors": {
            "type": "string",
            "description": "Access-Control-Allow-Origin answered for the given origin"
          },
          "deny": {
            "type": "string",
            "description": "X-VS-Deny (token | geo)"
          },
          "range": {
            "type": "string",
            "description": "Content-Range of the ranged segment request"
          },
          "verdict": {
            "type": "string",
            "description": "ok | token_rejected | geo_blocked | not_found | error | skipped | http_<code>"
          },
          "error": {
            "type": "string"
          },
          "note": {
            "type": "string"
          }
        }
      }
    }
  }
}
