{
  "openapi": "3.1.0",
  "info": {
    "title": "Grade",
    "version": "359bbfd6",
    "description": "Catálogo IPTV, biblioteca pessoal com feeds e informações para produtores sobre transmissão autorizada sob consulta."
  },
  "servers": [
    {
      "url": "https://staging.gradetv.net"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "ipt_… (convidado); a conta é cookie, nunca bearer"
      },
      "globalAccount": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-mm-auth",
        "description": "Global session in the product's HttpOnly cookie; writes require exact Origin and X-CSRF-Token."
      },
      "contaChaveApi": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "mmk_…",
        "description": "Account API key: `Authorization: Bearer mmk_…` or `X-Api-Key: mmk_…`. Created on the account page (API keys), valid only in the product where it was created; it acts as the account (or the organization that owns it)."
      }
    },
    "schemas": {
      "Saude": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Sempre `true` quando o Worker responde."
          },
          "app": {
            "type": "string",
            "description": "Nome do produto."
          },
          "build": {
            "type": "string",
            "description": "Commit publicado; o CI passa o SHA curto no deploy."
          },
          "ts": {
            "type": "string",
            "description": "Momento da resposta (UTC, ISO-8601)."
          },
          "catalog": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FrescorCatalogo"
              }
            ],
            "description": "Idade do catálogo: `synced_at` da última recarga e se passou do limite de 2 dias (o smoke reprova)."
          },
          "sources": {
            "type": "object",
            "description": "Uma entrada por referência do catálogo: `fetched_at`, `sha256`, `itens`, `age_hours`, `limit_days`, `stale` (passou do limite ou a recarga usou o snapshot anterior), `ausente` (saiu sem a fonte). Só o tronco velho reprova o smoke."
          }
        },
        "required": [
          "ok",
          "app",
          "build",
          "ts",
          "catalog",
          "sources"
        ],
        "description": "A resposta de `/api/health`: liveness, o commit publicado e a idade do catálogo."
      },
      "FrescorCatalogo": {
        "type": "object",
        "properties": {
          "synced_at": {
            "type": "string",
            "description": "Instante (ISO-8601) da última recarga que entrou no ar; `null` se nunca.",
            "nullable": true
          },
          "age_hours": {
            "type": "integer",
            "description": "Horas inteiras desde `synced_at`; `null` sem carimbo.",
            "nullable": true
          },
          "stale": {
            "type": "boolean",
            "description": "`true` quando passou de `limit_days` — o cron avisa por e-mail e o smoke reprova."
          },
          "limit_days": {
            "type": "integer",
            "description": "O limite em dias (2)."
          }
        },
        "required": [
          "synced_at",
          "age_hours",
          "stale",
          "limit_days"
        ],
        "description": "Idade do catálogo, como sai em `/api/health`."
      },
      "PaginaDeCanais": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Canal"
            },
            "description": "Os canais desta página, na ordem pedida (`sort`): nome, ou melhor saúde medida primeiro."
          },
          "total": {
            "type": "integer",
            "description": "Canais que casam com o filtro, ignorando a paginação."
          },
          "limit": {
            "type": "integer",
            "description": "Tamanho de página aplicado (teto de 50)."
          },
          "offset": {
            "type": "integer",
            "description": "Deslocamento aplicado nesta página."
          },
          "next_offset": {
            "type": "integer",
            "description": "Offset da próxima página; `null` quando acabou.",
            "nullable": true
          },
          "facets": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FacetasCanal"
              }
            ],
            "description": "Contagem por categoria DENTRO do filtro atual — serve para montar o menu lateral."
          },
          "filters": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FiltrosCanal"
              }
            ],
            "description": "Os filtros como o servidor os entendeu, já normalizados."
          }
        },
        "required": [
          "items",
          "total",
          "limit",
          "offset",
          "next_offset",
          "facets",
          "filters"
        ],
        "description": "A resposta da busca de canais. Não usa o envelope `Pagina<T>` porque troca `api` por `facets` e `filters`."
      },
      "Canal": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID estável do catálogo, ex. `GloboNews.br`. É a chave em toda a API."
          },
          "name": {
            "type": "string",
            "description": "Nome de exibição do canal."
          },
          "alt_names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Outros nomes pelos quais o canal é conhecido."
          },
          "country": {
            "type": "string",
            "description": "País de origem, ISO 3166-1 alpha-2.",
            "nullable": true
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "IDs de categoria do catálogo, ex. `news`, `sports`."
          },
          "category_labels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Os mesmos IDs já traduzidos para exibição."
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Idiomas do canal, ISO 639-3."
          },
          "language_labels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nomes dos idiomas acima, quando conhecidos."
          },
          "logo_url": {
            "type": "string",
            "description": "Logo servido por nós (variante ≤256px), não a origem.",
            "nullable": true
          },
          "website": {
            "type": "string",
            "description": "Site oficial do canal.",
            "nullable": true
          },
          "playable_hint": {
            "type": "boolean",
            "description": "Se a última verificação achou stream utilizável."
          },
          "slug": {
            "type": "string",
            "description": "Identificador legível; é id de API, não URL pública.",
            "nullable": true
          },
          "network": {
            "type": "string",
            "description": "Rede/emissora a que o canal pertence.",
            "nullable": true
          },
          "owners": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Quem opera o canal, conforme o cadastro."
          },
          "launched": {
            "type": "string",
            "description": "Data de lançamento (AAAA-MM-DD).",
            "nullable": true
          },
          "replaced_by": {
            "type": "string",
            "description": "ID do canal que substituiu este, se foi descontinuado.",
            "nullable": true
          },
          "feed_name": {
            "type": "string",
            "description": "Nome do feed quando o canal tem mais de um.",
            "nullable": true
          },
          "feed_format": {
            "type": "string",
            "description": "Formato do feed declarado pela fonte.",
            "nullable": true
          },
          "timezones": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Fusos em que o canal transmite."
          },
          "broadcast_area": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Área de cobertura, em códigos do catálogo."
          },
          "quality": {
            "type": "string",
            "description": "Melhor qualidade conhecida, ex. `1080p`."
          },
          "has_guide": {
            "type": "boolean",
            "description": "Se existe grade de programação (EPG) para este canal."
          },
          "guide_site": {
            "type": "string",
            "description": "Site de onde a grade vem.",
            "nullable": true
          },
          "guide_lang": {
            "type": "string",
            "description": "Idioma da grade de programação.",
            "nullable": true
          },
          "subdivision": {
            "type": "string",
            "description": "Estado/província, código do catálogo.",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "Cidade, código do catálogo.",
            "nullable": true
          },
          "kind": {
            "type": "string",
            "description": "`tv` ou `radio` (estação de rádio)."
          },
          "radio": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Radio"
              }
            ],
            "description": "Só em estação de rádio: tags, votos, cliques, codec, bitrate e localização."
          },
          "origem": {
            "type": "string",
            "description": "Identificador de procedência; créditos e licenças em `/sobre`."
          },
          "guide_now": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GuiaAgora"
              }
            ],
            "description": "Só na ficha: agora e a seguir na programação de hoje, grabada por nós; `null` sem guia fresca.",
            "nullable": true
          },
          "health_ext": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SaudeMedida"
              }
            ],
            "description": "Saúde medida por terceiro; `null` quando o canal não foi medido.",
            "nullable": true
          },
          "social": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Social"
              }
            ],
            "description": "Contadores da comunidade; ausente nas páginas HTML de SEO."
          },
          "api": {
            "type": "string",
            "description": "URL absoluta da ficha deste canal."
          }
        },
        "required": [
          "id",
          "name",
          "alt_names",
          "country",
          "categories",
          "category_labels",
          "languages",
          "language_labels",
          "logo_url",
          "website",
          "slug",
          "network",
          "owners",
          "launched",
          "replaced_by",
          "feed_name",
          "feed_format",
          "timezones",
          "broadcast_area",
          "has_guide",
          "guide_site",
          "guide_lang",
          "subdivision",
          "city",
          "kind",
          "origem",
          "health_ext",
          "api"
        ],
        "description": "Um canal do catálogo público, do jeito que a busca e a ficha devolvem."
      },
      "Radio": {
        "type": "object",
        "properties": {
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags da estação, vocabulário livre da fonte."
          },
          "votes": {
            "type": "integer",
            "description": "Votos registrados para a estação."
          },
          "clicks": {
            "type": "integer",
            "description": "Cliques registrados para a estação."
          },
          "codec": {
            "type": "string",
            "description": "Codec do stream (`MP3`, `AAC+`…), quando a fonte sabe.",
            "nullable": true
          },
          "bitrate": {
            "type": "integer",
            "description": "Bitrate em kbps, quando a fonte sabe.",
            "nullable": true
          },
          "geo": {
            "type": "object",
            "description": "`{ lat, lon }` da estação, quando a fonte tem; senão `null`.",
            "nullable": true
          }
        },
        "required": [
          "tags",
          "votes",
          "clicks",
          "codec",
          "bitrate",
          "geo"
        ],
        "description": "Informações próprias de uma estação de rádio. Vem em `Canal.radio` quando `kind` é `radio`."
      },
      "GuiaAgora": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "description": "Dia grabado, YYYY-MM-DD (UTC do grabber)."
          },
          "site": {
            "type": "string",
            "description": "Referência da programação, quando disponível.",
            "nullable": true
          },
          "agora": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Programa"
              }
            ],
            "description": "O programa no ar neste instante; `null` fora da grade.",
            "nullable": true
          },
          "a_seguir": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Programa"
              }
            ],
            "description": "O próximo programa; `null` no fim da grade.",
            "nullable": true
          }
        },
        "required": [
          "day",
          "site",
          "agora",
          "a_seguir"
        ],
        "description": "Resumo da guia do dia que a ficha carrega: o programa de agora e o próximo."
      },
      "Programa": {
        "type": "object",
        "properties": {
          "inicio": {
            "type": "string",
            "description": "Começo do programa, ISO 8601."
          },
          "fim": {
            "type": "string",
            "description": "Fim do programa, ISO 8601."
          },
          "titulo": {
            "type": "string",
            "description": "Título como o site da programação escreve."
          },
          "desc": {
            "type": "string",
            "description": "Sinopse curta (até 300 caracteres)."
          },
          "categoria": {
            "type": "string",
            "description": "Categoria do site, quando ele dá."
          }
        },
        "required": [
          "inicio",
          "fim",
          "titulo"
        ],
        "description": "Um programa da grade do dia, com os instantes em ISO 8601 (UTC)."
      },
      "SaudeMedida": {
        "type": "object",
        "properties": {
          "score": {
            "type": "integer",
            "description": "0–100, média móvel das sondagens do melhor stream do canal.",
            "nullable": true
          },
          "online": {
            "type": "boolean",
            "description": "Algum stream do canal respondeu `online` numa sondagem das 48 h anteriores à última recarga do catálogo."
          },
          "checked_at": {
            "type": "string",
            "description": "Instante (ISO-8601) da sondagem mais recente gravada; a recarga só o regrava quando algo mais do canal mudou ou a cada 7 dias, então pode estar até uma semana atrás da sondagem real.",
            "nullable": true
          }
        },
        "required": [
          "score",
          "online",
          "checked_at"
        ],
        "description": "Saúde observada do canal. É o melhor stream medido; `null` quando ninguém mediu — não medido não é ruim."
      },
      "Social": {
        "type": "object",
        "properties": {
          "plays": {
            "type": "integer",
            "description": "Relatos de que o canal tocou."
          },
          "fails": {
            "type": "integer",
            "description": "Relatos de que o canal falhou."
          },
          "favorites": {
            "type": "integer",
            "description": "Quantas pessoas favoritaram — conta pessoas, não cliques."
          },
          "comments": {
            "type": "integer",
            "description": "Comentários públicos no canal."
          },
          "last_fail_code": {
            "type": "string",
            "description": "Código da falha mais recente relatada.",
            "nullable": true
          },
          "health": {
            "type": "integer",
            "description": "Percentual de sucesso; `null` enquanto houver menos de 3 relatos.",
            "nullable": true
          },
          "your_plays": {
            "type": "integer",
            "description": "Relatos de sucesso no SEU navegador, sistema e país."
          },
          "your_fails": {
            "type": "integer",
            "description": "Relatos de falha no seu ambiente — é o que distingue 'fora do ar' de 'bloqueado para você'."
          },
          "your_fail_code": {
            "type": "string",
            "description": "Código da última falha no seu ambiente.",
            "nullable": true
          },
          "your_country_ok": {
            "type": "integer",
            "description": "Relatos de sucesso no SEU país, sem quebrar por navegador/SO."
          },
          "your_country_fail": {
            "type": "integer",
            "description": "Relatos de falha no seu país."
          },
          "your_geo_ok": {
            "type": "boolean",
            "description": "`false` = todas as tentativas relatadas no seu país falharam (suspeita de geo-bloqueio); `null` sem relato suficiente.",
            "nullable": true
          },
          "your_latency_ms": {
            "type": "integer",
            "description": "Latência média da playlist medida no hop, na borda do seu país; `null` sem medição.",
            "nullable": true
          },
          "your_latency_grade": {
            "type": "string",
            "description": "`otima`, `boa`, `lenta` ou `ruim`; `null` sem medição.",
            "nullable": true
          }
        },
        "required": [
          "plays",
          "fails",
          "favorites",
          "comments",
          "last_fail_code",
          "health",
          "your_plays",
          "your_fails",
          "your_fail_code",
          "your_country_ok",
          "your_country_fail",
          "your_geo_ok",
          "your_latency_ms",
          "your_latency_grade"
        ],
        "description": "Contadores da comunidade sobre um canal, incluindo o recorte do ambiente e do país de quem pediu."
      },
      "FacetasCanal": {
        "type": "object",
        "properties": {
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FacetaCategoria"
            },
            "description": "Categorias presentes no resultado, com a contagem de cada uma."
          }
        },
        "required": [
          "categories"
        ],
        "description": "Recortes da busca atual. Hoje só categoria; o formato aceita mais sem quebrar cliente."
      },
      "FacetaCategoria": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID da categoria no catálogo, ex. `news`."
          },
          "name": {
            "type": "string",
            "description": "Nome da categoria para exibição."
          },
          "count": {
            "type": "integer",
            "description": "Canais desta categoria dentro do filtro atual."
          },
          "icon": {
            "type": "string",
            "description": "Nome do ícone usado na interface.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "name",
          "count",
          "icon"
        ],
        "description": "Categoria com a contagem dentro da busca que acabou de ser feita."
      },
      "FiltrosCanal": {
        "type": "object",
        "properties": {
          "q": {
            "type": "string",
            "description": "Busca textual aplicada.",
            "nullable": true
          },
          "country": {
            "type": "string",
            "description": "País aplicado, já em maiúsculas.",
            "nullable": true
          },
          "category": {
            "type": "string",
            "description": "Categoria aplicada.",
            "nullable": true
          },
          "language": {
            "type": "string",
            "description": "Idioma aplicado, já normalizado para ISO 639-3.",
            "nullable": true
          },
          "network": {
            "type": "string",
            "description": "Rede aplicada.",
            "nullable": true
          },
          "quality": {
            "type": "string",
            "description": "Qualidade aplicada.",
            "nullable": true
          },
          "guide": {
            "type": "boolean",
            "description": "Se o filtro de grade de programação estava ligado."
          },
          "subdivision": {
            "type": "string",
            "description": "Subdivisão aplicada.",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "Cidade aplicada.",
            "nullable": true
          },
          "playable": {
            "type": "boolean",
            "description": "Se só canal com stream utilizável entrou."
          },
          "sort": {
            "type": "string",
            "description": "Ordem aplicada: `name`, `score` (saúde medida por terceiro) ou `votes` (rádio)."
          },
          "online": {
            "type": "boolean",
            "description": "Se só canal visto online pela fonte nas 48 h anteriores à última recarga entrou."
          },
          "kind": {
            "type": "string",
            "description": "`tv`, `radio` ou `all` — sem `kind` na chamada, é `tv`."
          },
          "tag": {
            "type": "string",
            "description": "Tag de rádio aplicada.",
            "nullable": true
          }
        },
        "required": [
          "q",
          "country",
          "category",
          "language",
          "network",
          "quality",
          "guide",
          "subdivision",
          "city",
          "playable",
          "sort",
          "online",
          "kind",
          "tag"
        ],
        "description": "O que o servidor entendeu do que você mandou — útil para saber por que um filtro não pegou."
      },
      "CanalCompleto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID estável do catálogo, ex. `GloboNews.br`. É a chave em toda a API."
          },
          "name": {
            "type": "string",
            "description": "Nome de exibição do canal."
          },
          "alt_names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Outros nomes pelos quais o canal é conhecido."
          },
          "country": {
            "type": "string",
            "description": "País de origem, ISO 3166-1 alpha-2.",
            "nullable": true
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "IDs de categoria do catálogo, ex. `news`, `sports`."
          },
          "category_labels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Os mesmos IDs já traduzidos para exibição."
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Idiomas do canal, ISO 639-3."
          },
          "language_labels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nomes dos idiomas acima, quando conhecidos."
          },
          "logo_url": {
            "type": "string",
            "description": "Logo servido por nós (variante ≤256px), não a origem.",
            "nullable": true
          },
          "website": {
            "type": "string",
            "description": "Site oficial do canal.",
            "nullable": true
          },
          "playable_hint": {
            "type": "boolean",
            "description": "Se a última verificação achou stream utilizável."
          },
          "slug": {
            "type": "string",
            "description": "Identificador legível; é id de API, não URL pública.",
            "nullable": true
          },
          "network": {
            "type": "string",
            "description": "Rede/emissora a que o canal pertence.",
            "nullable": true
          },
          "owners": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Quem opera o canal, conforme o cadastro."
          },
          "launched": {
            "type": "string",
            "description": "Data de lançamento (AAAA-MM-DD).",
            "nullable": true
          },
          "replaced_by": {
            "type": "string",
            "description": "ID do canal que substituiu este, se foi descontinuado.",
            "nullable": true
          },
          "feed_name": {
            "type": "string",
            "description": "Nome do feed quando o canal tem mais de um.",
            "nullable": true
          },
          "feed_format": {
            "type": "string",
            "description": "Formato do feed declarado pela fonte.",
            "nullable": true
          },
          "timezones": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Fusos em que o canal transmite."
          },
          "broadcast_area": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Área de cobertura, em códigos do catálogo."
          },
          "quality": {
            "type": "string",
            "description": "Melhor qualidade conhecida, ex. `1080p`."
          },
          "has_guide": {
            "type": "boolean",
            "description": "Se existe grade de programação (EPG) para este canal."
          },
          "guide_site": {
            "type": "string",
            "description": "Site de onde a grade vem.",
            "nullable": true
          },
          "guide_lang": {
            "type": "string",
            "description": "Idioma da grade de programação.",
            "nullable": true
          },
          "subdivision": {
            "type": "string",
            "description": "Estado/província, código do catálogo.",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "Cidade, código do catálogo.",
            "nullable": true
          },
          "kind": {
            "type": "string",
            "description": "`tv` ou `radio` (estação de rádio)."
          },
          "radio": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Radio"
              }
            ],
            "description": "Só em estação de rádio: tags, votos, cliques, codec, bitrate e localização."
          },
          "origem": {
            "type": "string",
            "description": "Identificador de procedência; créditos e licenças em `/sobre`."
          },
          "guide_now": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GuiaAgora"
              }
            ],
            "description": "Só na ficha: agora e a seguir na programação de hoje, grabada por nós; `null` sem guia fresca.",
            "nullable": true
          },
          "health_ext": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SaudeMedida"
              }
            ],
            "description": "Saúde medida por terceiro; `null` quando o canal não foi medido.",
            "nullable": true
          },
          "social": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Social"
              }
            ],
            "description": "Contadores da comunidade; ausente nas páginas HTML de SEO."
          },
          "api": {
            "type": "string",
            "description": "URL absoluta da ficha deste canal."
          },
          "streams": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Stream"
            },
            "description": "Transmissões conhecidas, com a URL já apontando para o nosso hop."
          },
          "_links": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LinksCanal"
              }
            ],
            "description": "Esta ficha, a mesma coisa na interface humana e o índice da API."
          }
        },
        "required": [
          "id",
          "name",
          "alt_names",
          "country",
          "categories",
          "category_labels",
          "languages",
          "language_labels",
          "logo_url",
          "website",
          "slug",
          "network",
          "owners",
          "launched",
          "replaced_by",
          "feed_name",
          "feed_format",
          "timezones",
          "broadcast_area",
          "has_guide",
          "guide_site",
          "guide_lang",
          "subdivision",
          "city",
          "kind",
          "origem",
          "health_ext",
          "api",
          "streams",
          "_links"
        ],
        "description": "A ficha de um canal: tudo que a busca traz, mais os streams e os links relacionados."
      },
      "Stream": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID do stream; é o `:id` de `GET /api/s/:id`."
          },
          "provider_url": {
            "type": "string",
            "description": "URL direta da transmissão no provedor, para copiar e colar em outro player após falha de acesso; null para protocolo não HTTP(S). O site oficial está em Canal.website.",
            "nullable": true
          },
          "legacy_url": {
            "type": "string",
            "description": "Player legado HTTP isolado para mídia HTTP. Null para origem incompatível; não dispensa CORS nem acesso à origem.",
            "nullable": true
          },
          "feed": {
            "type": "string",
            "description": "Qual feed do canal este stream serve.",
            "nullable": true
          },
          "title": {
            "type": "string",
            "description": "Título do stream, quando a fonte declara.",
            "nullable": true
          },
          "url": {
            "type": "string",
            "description": "URL de reprodução no nosso hop — conta o play (uma vez por pessoa, canal e dia) e devolve a playlist."
          },
          "quality": {
            "type": "string",
            "description": "Qualidade declarada deste stream.",
            "nullable": true
          },
          "needs_headers": {
            "type": "boolean",
            "description": "Se a origem exige Referer/User-Agent — o hop cuida disso."
          },
          "label": {
            "type": "string",
            "description": "Rótulo curto para escolher entre streams.",
            "nullable": true
          },
          "scheme": {
            "type": "string",
            "description": "Esquema da URL de origem: `http`, `https`, ou `youtube` (transmissão no YouTube, tocável só pelo player do site, nunca pelo M3U)."
          },
          "kind": {
            "type": "string",
            "description": "Como tocar: `hls`, `dash`, `ts`, `flv`, `audio` (rádio contínua), `youtube` (player oficial embutido) ou `externo` (RTMP/RTSP)."
          },
          "youtube": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Youtube"
              }
            ],
            "description": "Só em stream do YouTube: ids e as URLs de embed e de assistir."
          },
          "playable_hint": {
            "type": "boolean",
            "description": "Se a última verificação achou este stream utilizável."
          },
          "origem": {
            "type": "string",
            "description": "Identificador de procedência; créditos e licenças em `/sobre`."
          },
          "health_ext": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SaudeMedidaStream"
              }
            ],
            "description": "A verificação mais recente deste stream; `null` quando não foi medido.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "provider_url",
          "legacy_url",
          "feed",
          "title",
          "url",
          "quality",
          "needs_headers",
          "label",
          "scheme",
          "kind",
          "playable_hint",
          "origem",
          "health_ext"
        ],
        "description": "Uma das transmissões de um canal. A `url` já é o hop nosso, não a origem."
      },
      "Youtube": {
        "type": "object",
        "properties": {
          "video": {
            "type": "string",
            "description": "Id do vídeo da live (11 caracteres), quando a fonte deu um vídeo."
          },
          "channel": {
            "type": "string",
            "description": "Id do canal (`UC…`), quando a fonte deu o canal: o embed abre a live corrente."
          },
          "embed": {
            "type": "string",
            "description": "URL do embed oficial sem cookies (`youtube-nocookie.com`)."
          },
          "assistir": {
            "type": "string",
            "description": "URL para abrir no YouTube (botão do player e destino do hop)."
          }
        },
        "required": [
          "embed",
          "assistir"
        ],
        "description": "Um stream que é uma live do YouTube: o player embute o oficial; M3U/XSPF não o levam."
      },
      "SaudeMedidaStream": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "`online`, `offline`, `blocked` (geo), `timeout`, `error` ou `unknown`."
          },
          "score": {
            "type": "integer",
            "description": "0–100, média móvel: uma falha só não derruba o stream.",
            "nullable": true
          },
          "checked_at": {
            "type": "string",
            "description": "Instante (ISO-8601) da sondagem gravada; regravado quando o status ou o score mudam, ou a cada 7 dias.",
            "nullable": true
          },
          "resolution": {
            "type": "string",
            "description": "Resolução observada, ex. `1080p`.",
            "nullable": true
          },
          "bitrate": {
            "type": "integer",
            "description": "Bitrate em bits por segundo, quando medido.",
            "nullable": true
          },
          "latency_ms": {
            "type": "integer",
            "description": "Tempo até o primeiro byte na sondagem, em ms.",
            "nullable": true
          }
        },
        "required": [
          "status",
          "score",
          "checked_at",
          "resolution",
          "bitrate",
          "latency_ms"
        ],
        "description": "A verificação mais recente deste stream; `null` quando o stream não foi medido."
      },
      "LinksCanal": {
        "type": "object",
        "properties": {
          "self": {
            "type": "string",
            "description": "Esta mesma ficha em JSON."
          },
          "app": {
            "type": "string",
            "description": "A mesma coisa na interface humana."
          },
          "api_index": {
            "type": "string",
            "description": "Índice auto-descrito da API."
          }
        },
        "required": [
          "self",
          "app",
          "api_index"
        ],
        "description": "URLs relacionadas, para o agente não montar caminho na mão."
      },
      "SaudeCanal": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "description": "Canal a que esta saúde se refere."
          },
          "plays": {
            "type": "integer",
            "description": "Relatos de sucesso, no mundo todo."
          },
          "fails": {
            "type": "integer",
            "description": "Relatos de falha, no mundo todo."
          },
          "favorites": {
            "type": "integer",
            "description": "Quantas pessoas favoritaram o canal."
          },
          "comments": {
            "type": "integer",
            "description": "Comentários públicos no canal."
          },
          "health": {
            "type": "integer",
            "description": "Percentual de sucesso; `null` com menos de `min_relatos`.",
            "nullable": true
          },
          "last_ok_at": {
            "type": "string",
            "description": "Último relato de sucesso (UTC).",
            "nullable": true
          },
          "last_fail_at": {
            "type": "string",
            "description": "Último relato de falha (UTC).",
            "nullable": true
          },
          "last_fail_code": {
            "type": "string",
            "description": "Código da falha mais recente.",
            "nullable": true
          },
          "min_relatos": {
            "type": "integer",
            "description": "Quantos relatos são necessários antes de calcular `health`."
          },
          "reasons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MotivoFalha"
            },
            "description": "Por que falhou, do motivo mais comum para o menos."
          },
          "environments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Ambiente"
            },
            "description": "O mesmo canal por navegador, sistema e país."
          },
          "your_environment": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Ambiente"
              }
            ],
            "description": "O recorte de QUEM ESTÁ CHAMANDO, deduzido do User-Agent e da borda."
          },
          "regions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RegiaoSaude"
            },
            "description": "O mesmo canal agregado por PAÍS — onde falha e onde funciona."
          },
          "geo": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoCanal"
              }
            ],
            "description": "Veredito do bloqueio: geo-restrito (falha numas regiões, funciona noutras) ou fora do ar (falha em todas)."
          },
          "latency": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LatenciaPais"
            },
            "description": "Quão rápido a playlist abre, por país — medido no hop `/api/s/:id`."
          },
          "your_country": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RegiaoSaude"
              }
            ],
            "description": "O recorte do PAÍS de quem está chamando, com a latência da borda dele."
          },
          "pra_voce": {
            "type": "string",
            "description": "Veredito final para quem está chamando: `geo_bloqueado`, `lenta`, `instavel`, `boa` ou `sem_dado`."
          },
          "codes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Todos os códigos de falha que o produto reconhece."
          },
          "_links": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LinksSaude"
              }
            ],
            "description": "Esta saúde, o canal e onde relatar."
          }
        },
        "required": [
          "channel_id",
          "plays",
          "fails",
          "favorites",
          "comments",
          "health",
          "last_ok_at",
          "last_fail_at",
          "last_fail_code",
          "min_relatos",
          "reasons",
          "environments",
          "your_environment",
          "regions",
          "geo",
          "latency",
          "your_country",
          "pra_voce",
          "codes",
          "_links"
        ],
        "description": "Por que um canal falha e para quem — o que separa 'está fora do ar' de 'está bloqueado no seu país'."
      },
      "MotivoFalha": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Código: `cors`, `geo`, `sumiu`, `codec`, `playlist`, `sem_resposta`, `protocolo`, `sem_stream`, `outro`."
          },
          "label": {
            "type": "string",
            "description": "O motivo em uma frase curta."
          },
          "hint": {
            "type": "string",
            "description": "O que a pessoa pode fazer a respeito."
          },
          "count": {
            "type": "integer",
            "description": "Quantos relatos trouxeram este código."
          },
          "last_at": {
            "type": "string",
            "description": "Relato mais recente com este código (UTC).",
            "nullable": true
          }
        },
        "required": [
          "code",
          "label",
          "hint",
          "count",
          "last_at"
        ],
        "description": "Um motivo de falha agregado, já com o texto que a interface mostra."
      },
      "Ambiente": {
        "type": "object",
        "properties": {
          "browser": {
            "type": "string",
            "description": "Navegador normalizado, ex. `Chrome`; `Outro` quando não dá para dizer."
          },
          "os": {
            "type": "string",
            "description": "Sistema normalizado, ex. `Android`."
          },
          "country": {
            "type": "string",
            "description": "País de quem relatou, alpha-2."
          },
          "label": {
            "type": "string",
            "description": "Os três acima numa frase, para exibir."
          },
          "plays": {
            "type": "integer",
            "description": "Relatos de sucesso neste ambiente."
          },
          "fails": {
            "type": "integer",
            "description": "Relatos de falha neste ambiente."
          },
          "health": {
            "type": "integer",
            "description": "Percentual de sucesso aqui; `null` sem relato bastante.",
            "nullable": true
          },
          "last_fail_code": {
            "type": "string",
            "description": "Código da última falha neste ambiente.",
            "nullable": true
          },
          "last_at": {
            "type": "string",
            "description": "Relato mais recente neste ambiente (UTC).",
            "nullable": true
          }
        },
        "required": [
          "browser",
          "os",
          "country",
          "label",
          "plays",
          "fails",
          "health",
          "last_fail_code",
          "last_at"
        ],
        "description": "O comportamento do canal num navegador, sistema e país específicos."
      },
      "RegiaoSaude": {
        "type": "object",
        "properties": {
          "country": {
            "type": "string",
            "description": "País, alpha-2; `ZZ` quando a borda não disse."
          },
          "plays": {
            "type": "integer",
            "description": "Relatos de sucesso neste país."
          },
          "fails": {
            "type": "integer",
            "description": "Relatos de falha neste país."
          },
          "health": {
            "type": "integer",
            "description": "Percentual de sucesso aqui; `null` sem relato bastante.",
            "nullable": true
          },
          "latency_ms": {
            "type": "integer",
            "description": "Latência média da playlist medida na borda deste país; só em `your_country`.",
            "nullable": true
          },
          "latency_grade": {
            "type": "string",
            "description": "Nota da latência: `otima`, `boa`, `lenta` ou `ruim`; só em `your_country`.",
            "nullable": true
          }
        },
        "required": [
          "country",
          "plays",
          "fails",
          "health",
          "latency_ms",
          "latency_grade"
        ],
        "description": "O comportamento do canal num país — sem quebrar por navegador/SO."
      },
      "GeoCanal": {
        "type": "object",
        "properties": {
          "tipo": {
            "type": "string",
            "description": "`geo` (falha numa região, funciona noutra), `down` (falha em toda região medida), `ok` ou `unknown` (sem relato suficiente)."
          },
          "bloqueado_em": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Regiões onde só há relato de falha (teto de 8; `ZZ` = país desconhecido)."
          },
          "funciona_em": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Países com pelo menos um sucesso (teto de 8)."
          },
          "label": {
            "type": "string",
            "description": "O veredito em uma frase; vazio quando não há o que dizer."
          }
        },
        "required": [
          "tipo",
          "bloqueado_em",
          "funciona_em",
          "label"
        ],
        "description": "O que separa 'está bloqueado onde eu moro' de 'saiu do ar para todo mundo'."
      },
      "LatenciaPais": {
        "type": "object",
        "properties": {
          "country": {
            "type": "string",
            "description": "País onde a medição aconteceu, alpha-2."
          },
          "samples": {
            "type": "integer",
            "description": "Quantas aberturas entraram na média."
          },
          "avg_ms": {
            "type": "integer",
            "description": "Tempo médio até a playlist chegar, em milissegundos."
          },
          "grade": {
            "type": "string",
            "description": "`otima` (<600ms), `boa` (<1500ms), `lenta` (<3500ms) ou `ruim`."
          },
          "grade_label": {
            "type": "string",
            "description": "A nota em uma palavra, para exibir."
          },
          "last_at": {
            "type": "string",
            "description": "Última medição (UTC).",
            "nullable": true
          }
        },
        "required": [
          "country",
          "samples",
          "avg_ms",
          "grade",
          "grade_label",
          "last_at"
        ],
        "description": "Quanto tempo a playlist leva para abrir, medido na borda do país — a Grade busca a origem por você, então sabe."
      },
      "LinksSaude": {
        "type": "object",
        "properties": {
          "self": {
            "type": "string",
            "description": "Este mesmo painel."
          },
          "channel": {
            "type": "string",
            "description": "Ficha do canal."
          },
          "report": {
            "type": "string",
            "description": "Onde mandar um relato novo."
          }
        },
        "required": [
          "self",
          "channel",
          "report"
        ],
        "description": "Endereços relacionados ao painel de saúde."
      },
      "GuiaDoDia": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "description": "ID do canal no catálogo."
          },
          "day": {
            "type": "string",
            "description": "Dia grabado, YYYY-MM-DD."
          },
          "site": {
            "type": "string",
            "description": "Site de programação de origem.",
            "nullable": true
          },
          "agora": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Programa"
              }
            ],
            "description": "O programa no ar neste instante.",
            "nullable": true
          },
          "a_seguir": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Programa"
              }
            ],
            "description": "O próximo programa.",
            "nullable": true
          },
          "programas": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Programa"
            },
            "description": "Todos os programas do dia, em ordem (até 200)."
          }
        },
        "required": [
          "channel_id",
          "day",
          "site",
          "agora",
          "a_seguir",
          "programas"
        ],
        "description": "A programação disponível para o dia de um canal."
      },
      "Geo": {
        "type": "object",
        "properties": {
          "country": {
            "type": "string",
            "description": "País a usar; cai em `BR` quando a borda não informa."
          },
          "detected": {
            "type": "string",
            "description": "O que a borda realmente detectou; `null` se nada.",
            "nullable": true
          },
          "language": {
            "type": "string",
            "description": "Idioma a usar; cai em `por` sem detecção."
          },
          "language_detected": {
            "type": "string",
            "description": "Idioma realmente detectado.",
            "nullable": true
          },
          "source": {
            "type": "string",
            "description": "`cf` quando veio da borda, `fallback` quando é o padrão."
          },
          "api": {
            "type": "string",
            "description": "URL absoluta desta rota."
          }
        },
        "required": [
          "country",
          "detected",
          "language",
          "language_detected",
          "source",
          "api"
        ],
        "description": "País e idioma sugeridos para quem está chamando."
      },
      "ListaPais": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pais"
            },
            "description": "Todos os itens; estas rotas não paginam."
          }
        },
        "required": [
          "items"
        ],
        "description": "Coleção pequena e completa — faceta de catálogo, que cabe inteira numa resposta."
      },
      "Pais": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2, ex. `BR`."
          },
          "name": {
            "type": "string",
            "description": "Nome do país."
          },
          "count": {
            "type": "integer",
            "description": "Canais tocáveis deste país."
          },
          "flag": {
            "type": "string",
            "description": "URL do SVG da bandeira, servido por nós."
          }
        },
        "required": [
          "code",
          "name",
          "count",
          "flag"
        ],
        "description": "País com canal tocável no catálogo."
      },
      "ListaTag": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tag"
            },
            "description": "Todos os itens; estas rotas não paginam."
          }
        },
        "required": [
          "items"
        ],
        "description": "Coleção pequena e completa — faceta de catálogo, que cabe inteira numa resposta."
      },
      "Tag": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "A tag como está na fonte, em minúsculas (ex. `mpb`)."
          },
          "name": {
            "type": "string",
            "description": "O mesmo texto, para exibir."
          },
          "count": {
            "type": "integer",
            "description": "Estações tocáveis com esta tag no recorte pedido."
          }
        },
        "required": [
          "id",
          "name",
          "count"
        ],
        "description": "Tag de estação de rádio, vocabulário livre do catálogo, com a contagem no recorte."
      },
      "ListaCategoria": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Categoria"
            },
            "description": "Todos os itens; estas rotas não paginam."
          }
        },
        "required": [
          "items"
        ],
        "description": "Coleção pequena e completa — faceta de catálogo, que cabe inteira numa resposta."
      },
      "Categoria": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID da categoria, ex. `movies`."
          },
          "name": {
            "type": "string",
            "description": "Nome para exibição."
          },
          "description": {
            "type": "string",
            "description": "O que a categoria abrange, segundo a fonte.",
            "nullable": true
          },
          "icon": {
            "type": "string",
            "description": "Nome do ícone usado na interface.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "name",
          "description",
          "icon"
        ],
        "description": "Categoria do vocabulário do catálogo, sem contagem."
      },
      "ListaIdioma": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Idioma"
            },
            "description": "Todos os itens; estas rotas não paginam."
          }
        },
        "required": [
          "items"
        ],
        "description": "Coleção pequena e completa — faceta de catálogo, que cabe inteira numa resposta."
      },
      "Idioma": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "ISO 639-3, ex. `por`."
          },
          "name": {
            "type": "string",
            "description": "Nome do idioma."
          },
          "count": {
            "type": "integer",
            "description": "Canais tocáveis neste idioma."
          }
        },
        "required": [
          "code",
          "name",
          "count"
        ],
        "description": "Idioma com canal tocável."
      },
      "ListaRede": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rede"
            },
            "description": "Todos os itens; estas rotas não paginam."
          }
        },
        "required": [
          "items"
        ],
        "description": "Coleção pequena e completa — faceta de catálogo, que cabe inteira numa resposta."
      },
      "Rede": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nome da rede, ex. `Globo`."
          },
          "count": {
            "type": "integer",
            "description": "Canais tocáveis desta rede."
          }
        },
        "required": [
          "name",
          "count"
        ],
        "description": "Rede/emissora com canal tocável."
      },
      "ListaQualidade": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Qualidade"
            },
            "description": "Todos os itens; estas rotas não paginam."
          }
        },
        "required": [
          "items"
        ],
        "description": "Coleção pequena e completa — faceta de catálogo, que cabe inteira numa resposta."
      },
      "Qualidade": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "A qualidade em si, ex. `1080p`."
          },
          "name": {
            "type": "string",
            "description": "Mesmo valor, para exibição."
          },
          "count": {
            "type": "integer",
            "description": "Streams nesta qualidade."
          }
        },
        "required": [
          "id",
          "name",
          "count"
        ],
        "description": "Qualidade distinta encontrada nos streams."
      },
      "ListaSubdivisao": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Subdivisao"
            },
            "description": "Todos os itens; estas rotas não paginam."
          }
        },
        "required": [
          "items"
        ],
        "description": "Coleção pequena e completa — faceta de catálogo, que cabe inteira numa resposta."
      },
      "Subdivisao": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Código do catálogo, ex. `BR-SP`."
          },
          "country": {
            "type": "string",
            "description": "País a que pertence, alpha-2."
          },
          "name": {
            "type": "string",
            "description": "Nome da subdivisão."
          },
          "count": {
            "type": "integer",
            "description": "Canais tocáveis nela."
          }
        },
        "required": [
          "code",
          "country",
          "name",
          "count"
        ],
        "description": "Estado/província com canal tocável."
      },
      "ListaCidade": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Cidade"
            },
            "description": "Todos os itens; estas rotas não paginam."
          }
        },
        "required": [
          "items"
        ],
        "description": "Coleção pequena e completa — faceta de catálogo, que cabe inteira numa resposta."
      },
      "Cidade": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Código do catálogo."
          },
          "country": {
            "type": "string",
            "description": "País, alpha-2."
          },
          "subdivision": {
            "type": "string",
            "description": "Subdivisão a que a cidade pertence.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Nome da cidade."
          },
          "count": {
            "type": "integer",
            "description": "Canais tocáveis nela."
          }
        },
        "required": [
          "code",
          "country",
          "subdivision",
          "name",
          "count"
        ],
        "description": "Cidade com canal tocável."
      },
      "Biblioteca": {
        "type": "object",
        "properties": {
          "owner": {
            "type": "string",
            "description": "Rótulo derivado do dono desta árvore — estável enquanto o feed for o mesmo, e não reversível. NÃO é o token de convidado nem o id da conta: o token é a credencial mestra, sem revogação, e o rótulo sai derivado dele com o token do feed."
          },
          "feeds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Feeds"
              }
            ],
            "description": "Feeds da biblioteca inteira, em quatro formatos."
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PastaBiblioteca"
            },
            "description": "As pastas do dono, na ordem que ele arrumou."
          }
        },
        "required": [
          "owner",
          "feeds",
          "categories"
        ],
        "description": "A galeria inteira do dono: pastas, sub-abas, canais e as URLs de feed de cada nível."
      },
      "Feeds": {
        "type": "object",
        "properties": {
          "m3u": {
            "type": "string",
            "description": "Playlist M3U — é o que se cola no VLC."
          },
          "m3u8": {
            "type": "string",
            "description": "Mesma playlist, extensão `.m3u8` para players que só aceitam ela."
          },
          "json": {
            "type": "string",
            "description": "Mesma lista em JSON, para quem programa em cima."
          },
          "xspf": {
            "type": "string",
            "description": "Mesma lista em XSPF, para players que preferem XML."
          }
        },
        "required": [
          "m3u",
          "m3u8",
          "json",
          "xspf"
        ],
        "description": "O mesmo conteúdo em quatro formatos. São URLs públicas, com o token do feed no caminho — quem tem o link tem o conteúdo."
      },
      "PastaBiblioteca": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID da pasta, `cat_…`."
          },
          "name": {
            "type": "string",
            "description": "Nome que o dono deu."
          },
          "slug": {
            "type": "string",
            "description": "Versão do nome usada na URL do feed."
          },
          "sort": {
            "type": "integer",
            "description": "Posição na ordenação do dono."
          },
          "last_group_id": {
            "type": "string",
            "description": "Sub-aba aberta por último — é o que devolve a pessoa ao lugar onde parou.",
            "nullable": true
          },
          "api": {
            "type": "string",
            "description": "URL absoluta desta pasta."
          },
          "feeds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Feeds"
              }
            ],
            "description": "Feeds só desta pasta."
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SubAba"
            },
            "description": "Sub-abas dentro da pasta."
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "sort",
          "last_group_id",
          "api",
          "feeds",
          "groups"
        ],
        "description": "Pasta (aba principal) da galeria do dono."
      },
      "SubAba": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID da sub-aba, `grp_…`."
          },
          "name": {
            "type": "string",
            "description": "Nome que o dono deu."
          },
          "slug": {
            "type": "string",
            "description": "Versão do nome usada na URL do feed."
          },
          "sort": {
            "type": "integer",
            "description": "Posição na ordenação dentro da pasta."
          },
          "last_item_id": {
            "type": "string",
            "description": "Último canal tocado nesta sub-aba.",
            "nullable": true
          },
          "api": {
            "type": "string",
            "description": "URL absoluta desta sub-aba."
          },
          "feeds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Feeds"
              }
            ],
            "description": "Feeds só desta sub-aba."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemBiblioteca"
            },
            "description": "Canais colocados aqui, na ordem do dono."
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "sort",
          "last_item_id",
          "api",
          "feeds",
          "items"
        ],
        "description": "Sub-aba dentro de uma pasta; é o nível que agrupa os canais."
      },
      "ItemBiblioteca": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID do item na biblioteca, `itm_…`."
          },
          "channel_id": {
            "type": "string",
            "description": "ID do canal no catálogo, ex. `GloboNews.br`."
          },
          "stream_id": {
            "type": "string",
            "description": "Stream escolhido para este item.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Nome do canal; cai no `channel_id` se o canal sumiu do catálogo."
          },
          "logo_url": {
            "type": "string",
            "description": "Logo servido por nós.",
            "nullable": true
          },
          "url": {
            "type": "string",
            "description": "URL de reprodução no hop; `null` quando o stream sumiu.",
            "nullable": true
          },
          "quality": {
            "type": "string",
            "description": "Qualidade do stream escolhido.",
            "nullable": true
          },
          "kind": {
            "type": "string",
            "description": "Tipo de mídia: `hls`, `mpd`, `mp4`…"
          },
          "playable_hint": {
            "type": "boolean",
            "description": "Se a última verificação achou o stream utilizável."
          },
          "stale": {
            "type": "boolean",
            "description": "`true` quando o canal ou o stream sumiu da fonte — o item fica, sem tocar."
          },
          "sort": {
            "type": "integer",
            "description": "Posição dentro da sub-aba."
          }
        },
        "required": [
          "id",
          "channel_id",
          "stream_id",
          "name",
          "logo_url",
          "url",
          "quality",
          "kind",
          "playable_hint",
          "stale",
          "sort"
        ],
        "description": "Um canal dentro de uma sub-aba, com o stream já escolhido."
      },
      "BibliotecaComPasta": {
        "type": "object",
        "properties": {
          "owner": {
            "type": "string",
            "description": "Rótulo derivado do dono desta árvore — estável enquanto o feed for o mesmo, e não reversível. NÃO é o token de convidado nem o id da conta: o token é a credencial mestra, sem revogação, e o rótulo sai derivado dele com o token do feed."
          },
          "feeds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Feeds"
              }
            ],
            "description": "Feeds da biblioteca inteira, em quatro formatos."
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PastaBiblioteca"
            },
            "description": "As pastas do dono, na ordem que ele arrumou."
          },
          "id": {
            "type": "string",
            "description": "ID da pasta criada, `cat_…`."
          },
          "name": {
            "type": "string",
            "description": "Nome da pasta criada."
          },
          "slug": {
            "type": "string",
            "description": "Slug da pasta — é o que entra na URL do feed dela."
          },
          "group_id": {
            "type": "string",
            "description": "ID da sub-aba Geral, criada junto com a pasta."
          }
        },
        "required": [
          "owner",
          "feeds",
          "categories",
          "id",
          "name",
          "slug",
          "group_id"
        ],
        "description": "A biblioteca inteira mais os ids da pasta que acabou de ser criada."
      },
      "BibliotecaComSubAba": {
        "type": "object",
        "properties": {
          "owner": {
            "type": "string",
            "description": "Rótulo derivado do dono desta árvore — estável enquanto o feed for o mesmo, e não reversível. NÃO é o token de convidado nem o id da conta: o token é a credencial mestra, sem revogação, e o rótulo sai derivado dele com o token do feed."
          },
          "feeds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Feeds"
              }
            ],
            "description": "Feeds da biblioteca inteira, em quatro formatos."
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PastaBiblioteca"
            },
            "description": "As pastas do dono, na ordem que ele arrumou."
          },
          "id": {
            "type": "string",
            "description": "ID da sub-aba criada, `grp_…`."
          },
          "name": {
            "type": "string",
            "description": "Nome da sub-aba criada."
          },
          "slug": {
            "type": "string",
            "description": "Slug da sub-aba — entra na URL do feed dela."
          },
          "category_id": {
            "type": "string",
            "description": "Pasta que recebeu a sub-aba."
          }
        },
        "required": [
          "owner",
          "feeds",
          "categories",
          "id",
          "name",
          "slug",
          "category_id"
        ],
        "description": "A biblioteca inteira mais os ids da sub-aba que acabou de ser criada."
      },
      "BibliotecaComItem": {
        "type": "object",
        "properties": {
          "owner": {
            "type": "string",
            "description": "Rótulo derivado do dono desta árvore — estável enquanto o feed for o mesmo, e não reversível. NÃO é o token de convidado nem o id da conta: o token é a credencial mestra, sem revogação, e o rótulo sai derivado dele com o token do feed."
          },
          "feeds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Feeds"
              }
            ],
            "description": "Feeds da biblioteca inteira, em quatro formatos."
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PastaBiblioteca"
            },
            "description": "As pastas do dono, na ordem que ele arrumou."
          },
          "id": {
            "type": "string",
            "description": "ID do item criado, `itm_…`."
          },
          "group_id": {
            "type": "string",
            "description": "Sub-aba que recebeu o canal."
          },
          "category_id": {
            "type": "string",
            "description": "Pasta a que essa sub-aba pertence."
          }
        },
        "required": [
          "owner",
          "feeds",
          "categories",
          "id",
          "group_id",
          "category_id"
        ],
        "description": "A biblioteca inteira mais onde o canal caiu — para a tela abrir na sub-aba certa."
      },
      "PaginaDeHistorico": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Visita"
            },
            "description": "Os itens desta página, na ordem que a rota define."
          },
          "total": {
            "type": "integer",
            "description": "Quantos itens casam com o filtro, ignorando a paginação."
          },
          "limit": {
            "type": "integer",
            "description": "Tamanho de página efetivamente aplicado (pode ser menor que o pedido)."
          },
          "offset": {
            "type": "integer",
            "description": "Deslocamento aplicado nesta página."
          },
          "next_offset": {
            "type": "integer",
            "description": "Offset da próxima página; `null` quando esta é a última.",
            "nullable": true
          },
          "next": {
            "type": "string",
            "description": "URL absoluta da próxima página, com os mesmos filtros — siga até vir `null` para varrer tudo, sem remontar a querystring.",
            "nullable": true
          },
          "api": {
            "type": "string",
            "description": "URL absoluta desta própria listagem."
          },
          "max": {
            "type": "integer",
            "description": "Quantos canais o histórico guarda no máximo."
          }
        },
        "required": [
          "items",
          "total",
          "limit",
          "offset",
          "next_offset",
          "next",
          "api",
          "max"
        ],
        "description": "Página do histórico do dono. `max` é o teto de linhas guardadas — passou disso, a mais antiga sai."
      },
      "Visita": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "description": "ID do canal assistido."
          },
          "name": {
            "type": "string",
            "description": "Nome do canal; cai no ID se ele saiu do catálogo."
          },
          "country": {
            "type": "string",
            "description": "País do canal.",
            "nullable": true
          },
          "logo_url": {
            "type": "string",
            "description": "Logo servido por nós.",
            "nullable": true
          },
          "playable_hint": {
            "type": "boolean",
            "description": "Se a última verificação achou stream utilizável."
          },
          "plays": {
            "type": "integer",
            "description": "Quantas vezes o dono assistiu este canal."
          },
          "first_at": {
            "type": "string",
            "description": "Primeira vez que assistiu (UTC)."
          },
          "last_at": {
            "type": "string",
            "description": "Última vez que assistiu (UTC)."
          },
          "stale": {
            "type": "boolean",
            "description": "`true` quando o canal não existe mais no catálogo."
          },
          "api": {
            "type": "string",
            "description": "URL absoluta da ficha do canal."
          }
        },
        "required": [
          "channel_id",
          "name",
          "country",
          "logo_url",
          "playable_hint",
          "plays",
          "first_at",
          "last_at",
          "stale",
          "api"
        ],
        "description": "Um canal no histórico do dono, com quantas vezes ele assistiu."
      },
      "PaginaDeRelatos": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Relato"
            },
            "description": "Os relatos, do mais recente para o mais antigo."
          },
          "limit": {
            "type": "integer",
            "description": "Tamanho de página aplicado."
          },
          "offset": {
            "type": "integer",
            "description": "Deslocamento aplicado."
          },
          "next_offset": {
            "type": "integer",
            "description": "Offset da próxima página; `null` quando acabou.",
            "nullable": true
          },
          "retention_days": {
            "type": "integer",
            "description": "Depois de quantos dias a linha é apagada."
          },
          "filters": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FiltrosRelato"
              }
            ],
            "description": "Os filtros como o servidor os entendeu."
          },
          "api": {
            "type": "string",
            "description": "URL absoluta desta listagem."
          }
        },
        "required": [
          "items",
          "limit",
          "offset",
          "next_offset",
          "retention_days",
          "filters",
          "api"
        ],
        "description": "Página de relatos crus. Não tem `total` de propósito: contar a tabela inteira a cada consulta de investigação sai caro e não muda a decisão de quem investiga."
      },
      "Relato": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "description": "Canal que a pessoa tentou assistir."
          },
          "ok": {
            "type": "boolean",
            "description": "Se tocou (`true`) ou falhou (`false`)."
          },
          "code": {
            "type": "string",
            "description": "Código da falha, quando falhou.",
            "nullable": true
          },
          "ip": {
            "type": "string",
            "description": "Endereço de quem relatou. Nunca aparece no painel público.",
            "nullable": true
          },
          "browser": {
            "type": "string",
            "description": "Navegador deduzido do User-Agent.",
            "nullable": true
          },
          "os": {
            "type": "string",
            "description": "Sistema deduzido do User-Agent.",
            "nullable": true
          },
          "country": {
            "type": "string",
            "description": "País deduzido pela borda.",
            "nullable": true
          },
          "day": {
            "type": "string",
            "description": "Dia do relato (AAAA-MM-DD) — a contagem é uma por dono, canal e dia."
          },
          "at": {
            "type": "string",
            "description": "Momento exato do relato (UTC)."
          }
        },
        "required": [
          "channel_id",
          "ok",
          "code",
          "ip",
          "browser",
          "os",
          "country",
          "day",
          "at"
        ],
        "description": "Relato cru de reprodução, com endereço — por isso a rota é só de operador e a linha expira."
      },
      "FiltrosRelato": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "description": "Canal filtrado, ou `null` para todos.",
            "nullable": true
          },
          "only_failures": {
            "type": "boolean",
            "description": "Se só as falhas entraram."
          }
        },
        "required": [
          "channel_id",
          "only_failures"
        ],
        "description": "O que o servidor entendeu do filtro de relatos."
      },
      "PaginaDeFavoritos": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Favorito"
            },
            "description": "Os itens desta página, na ordem que a rota define."
          },
          "total": {
            "type": "integer",
            "description": "Quantos itens casam com o filtro, ignorando a paginação."
          },
          "limit": {
            "type": "integer",
            "description": "Tamanho de página efetivamente aplicado (pode ser menor que o pedido)."
          },
          "offset": {
            "type": "integer",
            "description": "Deslocamento aplicado nesta página."
          },
          "next_offset": {
            "type": "integer",
            "description": "Offset da próxima página; `null` quando esta é a última.",
            "nullable": true
          },
          "next": {
            "type": "string",
            "description": "URL absoluta da próxima página, com os mesmos filtros — siga até vir `null` para varrer tudo, sem remontar a querystring.",
            "nullable": true
          },
          "api": {
            "type": "string",
            "description": "URL absoluta desta própria listagem."
          },
          "max": {
            "type": "integer",
            "description": "Quantos favoritos o dono pode ter."
          }
        },
        "required": [
          "items",
          "total",
          "limit",
          "offset",
          "next_offset",
          "next",
          "api",
          "max"
        ],
        "description": "Página dos favoritos do dono, com o teto de quantos cabem."
      },
      "Favorito": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "description": "ID do canal favoritado."
          },
          "name": {
            "type": "string",
            "description": "Nome do canal; cai no ID se ele saiu do catálogo."
          },
          "country": {
            "type": "string",
            "description": "País do canal.",
            "nullable": true
          },
          "quality": {
            "type": "string",
            "description": "Melhor qualidade conhecida.",
            "nullable": true
          },
          "logo_url": {
            "type": "string",
            "description": "Logo servido por nós.",
            "nullable": true
          },
          "playable_hint": {
            "type": "boolean",
            "description": "Se a última verificação achou stream utilizável."
          },
          "favorites": {
            "type": "integer",
            "description": "Quantas pessoas favoritaram este canal ao todo."
          },
          "created_at": {
            "type": "string",
            "description": "Quando o dono favoritou (UTC)."
          },
          "stale": {
            "type": "boolean",
            "description": "`true` quando o canal não existe mais no catálogo."
          },
          "api": {
            "type": "string",
            "description": "URL absoluta da ficha do canal."
          }
        },
        "required": [
          "channel_id",
          "name",
          "country",
          "quality",
          "logo_url",
          "playable_hint",
          "favorites",
          "created_at",
          "stale",
          "api"
        ],
        "description": "Um canal favoritado pelo dono."
      },
      "PaginaDeComentarios": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Comentario"
            },
            "description": "Os itens desta página, na ordem que a rota define."
          },
          "total": {
            "type": "integer",
            "description": "Quantos itens casam com o filtro, ignorando a paginação."
          },
          "limit": {
            "type": "integer",
            "description": "Tamanho de página efetivamente aplicado (pode ser menor que o pedido)."
          },
          "offset": {
            "type": "integer",
            "description": "Deslocamento aplicado nesta página."
          },
          "next_offset": {
            "type": "integer",
            "description": "Offset da próxima página; `null` quando esta é a última.",
            "nullable": true
          },
          "next": {
            "type": "string",
            "description": "URL absoluta da próxima página, com os mesmos filtros — siga até vir `null` para varrer tudo, sem remontar a querystring.",
            "nullable": true
          },
          "api": {
            "type": "string",
            "description": "URL absoluta desta própria listagem."
          },
          "channel_id": {
            "type": "string",
            "description": "Canal a que os comentários pertencem."
          },
          "max_length": {
            "type": "integer",
            "description": "Tamanho máximo de um comentário novo."
          }
        },
        "required": [
          "items",
          "total",
          "limit",
          "offset",
          "next_offset",
          "next",
          "api",
          "channel_id",
          "max_length"
        ],
        "description": "Página de comentários de um canal, com o teto de tamanho de quem for escrever a seguir."
      },
      "Comentario": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID do comentário, para apagar."
          },
          "channel_id": {
            "type": "string",
            "description": "Canal em que o comentário foi escrito."
          },
          "author": {
            "type": "string",
            "description": "Apelido de quem escreveu; `Visitante` quando não informado."
          },
          "body": {
            "type": "string",
            "description": "O texto do comentário."
          },
          "created_at": {
            "type": "string",
            "description": "Quando foi escrito (UTC, com milissegundos)."
          },
          "mine": {
            "type": "boolean",
            "description": "`true` se é seu — só você pode apagar."
          },
          "api": {
            "type": "string",
            "description": "URL absoluta deste comentário."
          }
        },
        "required": [
          "id",
          "channel_id",
          "author",
          "body",
          "created_at",
          "mine",
          "api"
        ],
        "description": "Comentário público num canal."
      },
      "MensagemChat": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID da mensagem dentro da sala."
          },
          "autor": {
            "type": "string",
            "description": "Apelido de quem mandou — estável por dono, gerado se não informado."
          },
          "body": {
            "type": "string",
            "description": "O texto da mensagem."
          },
          "at": {
            "type": "string",
            "description": "Quando foi mandada (UTC)."
          }
        },
        "required": [
          "id",
          "autor",
          "body",
          "at"
        ],
        "description": "Mensagem da sala de chat de um canal."
      },
      "LinksChat": {
        "type": "object",
        "properties": {
          "self": {
            "type": "string",
            "description": "Esta mesma listagem por HTTP."
          },
          "websocket": {
            "type": "string",
            "description": "URL `wss://` da sala; mande `{t:'hello',token}` antes de falar."
          },
          "channel": {
            "type": "string",
            "description": "Ficha do canal a que a sala pertence."
          }
        },
        "required": [
          "self",
          "websocket",
          "channel"
        ],
        "description": "Endereços da sala — incluindo o WebSocket, que é o caminho ao vivo."
      },
      "Conta": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID da conta."
          },
          "email": {
            "type": "string",
            "description": "E-mail confirmado por código."
          }
        },
        "required": [
          "id",
          "email"
        ],
        "description": "A pessoa por trás da sessão."
      },
      "Recursos": {
        "type": "object",
        "properties": {
          "categories": {
            "type": "integer",
            "description": "Quantas pastas o dono tem."
          },
          "favorites": {
            "type": "integer",
            "description": "Quantos canais ele favoritou."
          },
          "history": {
            "type": "integer",
            "description": "Quantos canais estão no histórico."
          }
        },
        "required": [
          "categories",
          "favorites",
          "history"
        ],
        "description": "O tamanho da biblioteca do dono — para o agente saber o que vai encontrar antes de buscar."
      },
      "Billing": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "description": "Sempre `x402` — é o único protocolo de cobrança aceito."
          },
          "mode": {
            "type": "string",
            "description": "Modo do vendedor: `live` cobra de verdade, `dev` libera sem pagar."
          },
          "network": {
            "type": "string",
            "description": "Rede da USDC: `base` em produção, `base-sepolia` em homologação."
          },
          "chain_id": {
            "type": "integer",
            "description": "Chain ID EVM da rede acima, para a carteira assinar na cadeia certa."
          },
          "pay_to": {
            "type": "string",
            "description": "Endereço que recebe o pagamento.",
            "nullable": true
          },
          "homolog": {
            "type": "boolean",
            "description": "Seam de homologação ligado: dá para fechar o loop sem gastar USDC."
          },
          "dev": {
            "type": "boolean",
            "description": "Modo de desenvolvimento: o 402 é simulado."
          },
          "dev_gate": {
            "type": "boolean",
            "description": "Há credencial de homolog configurada; não concede acesso."
          },
          "gratis": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "SKUs temporariamente gratuitos."
          },
          "facilitator": {
            "type": "string",
            "description": "URL do facilitador que verifica e liquida o pagamento."
          },
          "asset": {
            "type": "string",
            "description": "Moeda aceita — sempre `USDC`."
          },
          "asset_address": {
            "type": "string",
            "description": "Contrato da USDC na rede acima."
          },
          "faucet": {
            "type": "string",
            "description": "Torneira de USDC de teste; só em base-sepolia.",
            "nullable": true
          },
          "wallets": {
            "type": "object",
            "description": "Links de carteiras que falam x402 (metamask, coinbase, base_app)."
          },
          "product": {
            "type": "string",
            "description": "Nome do produto que está cobrando."
          },
          "prices": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Precos"
              }
            ],
            "description": "Quanto custa cada ação paga, em USD."
          },
          "chat": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Passe"
              }
            ],
            "description": "Seu passe de chat; sem credencial vem inativo."
          },
          "limits": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TetosGaleria"
              }
            ],
            "description": "Quantas pastas, sub-abas e canais cabem."
          }
        },
        "required": [
          "provider",
          "mode",
          "network",
          "chain_id",
          "pay_to",
          "homolog",
          "dev",
          "dev_gate",
          "facilitator",
          "asset",
          "asset_address",
          "faucet",
          "wallets",
          "product",
          "prices",
          "chat",
          "limits"
        ],
        "description": "Tudo que decide se uma chamada vai custar: a configuração x402, os preços do produto, o seu passe e os tetos da galeria."
      },
      "Precos": {
        "type": "object",
        "properties": {
          "contact_agent_usd": {
            "type": "number",
            "description": "Custo de um contato de agente."
          },
          "chat_month_usd": {
            "type": "number",
            "description": "Custo do passe de chat por 30 dias."
          },
          "abuso_24h_usd": {
            "type": "number",
            "description": "Preço da porta de UA vazio ou curl (desligada)."
          }
        },
        "required": [
          "contact_agent_usd",
          "chat_month_usd",
          "abuso_24h_usd"
        ],
        "description": "Preços em vigor, em dólar. Leia daqui, não da documentação."
      },
      "Passe": {
        "type": "object",
        "properties": {
          "active": {
            "type": "boolean",
            "description": "Se o passe vale neste momento."
          },
          "until": {
            "type": "string",
            "description": "Até quando vale (UTC); `null` sem passe, ou com o chat aberto de graça (`X402_GRATIS`).",
            "nullable": true
          }
        },
        "required": [
          "active",
          "until"
        ],
        "description": "Passe mensal do chat: o `direito` `chat_pass` de quem fala, no registro global de compras."
      },
      "TetosGaleria": {
        "type": "object",
        "properties": {
          "categories": {
            "type": "integer",
            "description": "Pastas por dono."
          },
          "groups_per_category": {
            "type": "integer",
            "description": "Sub-abas por pasta."
          },
          "items_per_group": {
            "type": "integer",
            "description": "Canais por sub-aba."
          }
        },
        "required": [
          "categories",
          "groups_per_category",
          "items_per_group"
        ],
        "description": "Os limites da galeria pessoal — existem para o catálogo público não ser despejado numa biblioteca."
      },
      "Ok": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Sempre `true` — a falha vem como status 4xx/5xx, não como `ok:false`."
          }
        },
        "required": [
          "ok"
        ],
        "description": "Confirmação de escrita que não tem corpo próprio a devolver."
      },
      "Metricas": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "Nome do produto."
          },
          "today": {
            "type": "string",
            "description": "Dia de referência (UTC, AAAA-MM-DD)."
          },
          "today_visits": {
            "type": "integer",
            "description": "Visitas contadas hoje."
          },
          "today_contacts": {
            "type": "integer",
            "description": "Contatos de hoje; só com `METRICS_TOKEN`."
          },
          "today_plays": {
            "type": "integer",
            "description": "Plays contados hoje."
          },
          "today_feeds": {
            "type": "integer",
            "description": "Feeds servidos hoje."
          },
          "days": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Um registro por dia da janela, com as contagens de cada métrica."
          },
          "usage": {
            "type": "object",
            "description": "Uso por recurso do produto — aqui, itens na galeria."
          },
          "accounts": {
            "type": "object",
            "description": "Vazio: convidado e conta são do SDK, sem tabela nem contagem por produto aqui."
          },
          "top_plays": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Canais mais tocados hoje: `channel_id`, `name` e `count`."
          },
          "financeiro": {
            "type": "object",
            "description": "`hoje_usd`, `hoje_count`, `rede`; só com `METRICS_TOKEN`."
          },
          "payments": {
            "type": "object",
            "description": "Resumo financeiro; só com METRICS_TOKEN."
          }
        },
        "required": [
          "app",
          "today",
          "today_visits",
          "today_plays",
          "today_feeds",
          "days",
          "usage",
          "accounts",
          "top_plays"
        ],
        "description": "Painel de 7 dias. O bloco `payments` só aparece com o token do operador e só em Base mainnet."
      },
      "EstadoCatalogo": {
        "type": "object",
        "properties": {
          "live": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ContagemCatalogo"
              }
            ],
            "description": "O catálogo que está servindo agora."
          },
          "staging": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ContagemCatalogo"
              }
            ],
            "description": "As tabelas `*_novo` da recarga em curso; `null` quando não há staging.",
            "nullable": true
          },
          "meta": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MetaCatalogo"
              }
            ],
            "description": "Carimbos da última recarga e do staging aberto."
          }
        },
        "required": [
          "live",
          "staging",
          "meta"
        ],
        "description": "O retrato que o serviço de recarga lê antes de começar e depois de trocar."
      },
      "ContagemCatalogo": {
        "type": "object",
        "properties": {
          "channels": {
            "type": "integer",
            "description": "Canais (linhas de `channels`)."
          },
          "channels_fts": {
            "type": "integer",
            "description": "Linhas do índice de busca (`channels_fts`)."
          },
          "channels_fts_ids": {
            "type": "integer",
            "description": "Ids distintos no índice de busca; a troca exige que seja igual a `channels`."
          },
          "streams": {
            "type": "integer",
            "description": "Streams (linhas de `streams`)."
          },
          "blocklist": {
            "type": "integer",
            "description": "Canais bloqueados (DMCA), que ficam fora do catálogo."
          },
          "facet_countries": {
            "type": "integer",
            "description": "Países na faceta."
          },
          "facet_categories": {
            "type": "integer",
            "description": "Categorias na faceta."
          },
          "facet_languages": {
            "type": "integer",
            "description": "Idiomas na faceta (ISO 639-3 inteiro)."
          },
          "facet_subdivisions": {
            "type": "integer",
            "description": "Estados e subdivisões na faceta."
          },
          "facet_cities": {
            "type": "integer",
            "description": "Cidades na faceta."
          }
        },
        "required": [
          "channels",
          "channels_fts",
          "channels_fts_ids",
          "streams",
          "blocklist",
          "facet_countries",
          "facet_categories",
          "facet_languages",
          "facet_subdivisions",
          "facet_cities"
        ],
        "description": "Quantas linhas cada tabela do catálogo tem — no ar ou no staging."
      },
      "MetaCatalogo": {
        "type": "object",
        "properties": {
          "synced_at": {
            "type": "string",
            "description": "Instante (ISO-8601) da última troca bem-sucedida; é o que `/api/health` compara com o limite de 2 dias.",
            "nullable": true
          },
          "applied_at": {
            "type": "string",
            "description": "Instante em que o catálogo novo entrou no ar.",
            "nullable": true
          },
          "dump_sha256": {
            "type": "string",
            "description": "SHA-256 do conjunto de dados que gerou o catálogo no ar.",
            "nullable": true
          },
          "staging_run": {
            "type": "string",
            "description": "`recarga_id` da execução com staging aberto agora; `null` sem staging.",
            "nullable": true
          }
        },
        "required": [
          "synced_at",
          "applied_at",
          "dump_sha256",
          "staging_run"
        ],
        "description": "Os carimbos da recarga, lidos de `catalog_meta`."
      },
      "SlugsPublicados": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SlugPublicado"
            },
            "description": "Os pares desta página."
          },
          "next_after": {
            "type": "string",
            "description": "Passe em `apos` para a próxima página; `null` quando acabou.",
            "nullable": true
          }
        },
        "required": [
          "items",
          "next_after"
        ],
        "description": "Página de slugs publicados, em ordem de id."
      },
      "SlugPublicado": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Id do canal no catálogo (ex.: `GloboRJ.br`)."
          },
          "slug": {
            "type": "string",
            "description": "Slug publicado; a troca recusa staging em que este id venha com outro."
          }
        },
        "required": [
          "id",
          "slug"
        ],
        "description": "O par que a recarga herda: id estável do canal → slug já publicado."
      },
      "GuiaLoteGravado": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Sempre `true`; lote recusado vem como 4xx."
          },
          "day": {
            "type": "string",
            "description": "O dia gravado."
          },
          "gravados": {
            "type": "integer",
            "description": "Canais que entraram (INSERT OR REPLACE: reenviar não duplica)."
          }
        },
        "required": [
          "ok",
          "day",
          "gravados"
        ],
        "description": "Resultado de um lote da guia."
      },
      "GuiaRegistrada": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Sempre `true`."
          },
          "guia": {
            "type": "object",
            "description": "O registro normalizado: `fetched_at`, `itens`, `stale`, `ausente`…"
          }
        },
        "required": [
          "ok",
          "guia"
        ],
        "description": "A fonte `guia` como ficou em `catalog_meta.fontes`."
      },
      "LogosMortas": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "`{ id, name, alt_names, country, logo_url, fail_count, last_error }` por canal."
          },
          "limit": {
            "type": "integer",
            "description": "O teto aplicado."
          }
        },
        "required": [
          "items",
          "limit"
        ],
        "description": "Canais com origem de logo morta, para o script de overrides casar."
      },
      "OverridesGravados": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Sempre `true`; pedido recusado vem como 4xx."
          },
          "fonte": {
            "type": "string",
            "description": "A fonte gravada em cada linha."
          },
          "gravados": {
            "type": "integer",
            "description": "Overrides que entraram (INSERT OR REPLACE)."
          }
        },
        "required": [
          "ok",
          "fonte",
          "gravados"
        ],
        "description": "Resultado da gravação de overrides de logo."
      },
      "DeltaAberto": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Sempre `true`; recusa vem como 409 com `problemas[]`."
          },
          "recarga_id": {
            "type": "string",
            "description": "A execução aberta — os pedidos seguintes repetem."
          },
          "live": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ContagemCatalogo"
              }
            ],
            "description": "O catálogo no ar, antes da diferença."
          }
        },
        "required": [
          "ok",
          "recarga_id",
          "live"
        ],
        "description": "A recarga por diferença passou pela coleira e está marcada; nada mudou no ar ainda."
      },
      "DeltaAplicado": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Sempre `true`; pedido recusado vem como 4xx."
          },
          "tabela": {
            "type": "string",
            "description": "A tabela tocada."
          },
          "recebidas": {
            "type": "integer",
            "description": "Soma das linhas de `upsert` e `alterar` aceitas."
          },
          "removidas_pedidas": {
            "type": "integer",
            "description": "Chaves de remoção aceitas, inclusive as já ausentes."
          },
          "gravadas": {
            "type": "integer",
            "description": "Linhas que entraram ou foram atualizadas (na FTS, contando os ids apagados)."
          },
          "removidas": {
            "type": "integer",
            "description": "Linhas apagadas por `remover`."
          }
        },
        "required": [
          "ok",
          "tabela",
          "recebidas",
          "removidas_pedidas",
          "gravadas",
          "removidas"
        ],
        "description": "Um pedido da diferença entrou no catálogo vivo, num batch."
      },
      "RelatorioDelta": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Sempre `true`; conferência que falha vem como 409 com `problemas[]`."
          },
          "recarga_id": {
            "type": "string",
            "description": "A recarga que entrou no ar."
          },
          "depois": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ContagemCatalogo"
              }
            ],
            "description": "O catálogo que está servindo agora."
          },
          "synced_at": {
            "type": "string",
            "description": "O `synced_at` gravado no mesmo batch do fechamento."
          }
        },
        "required": [
          "ok",
          "recarga_id",
          "depois",
          "synced_at"
        ],
        "description": "A diferença fechou: o catálogo inteiro bateu com `esperado` e o carimbo foi gravado."
      },
      "PaymentQuota": {
        "type": "object",
        "properties": {
          "free": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentFree"
            },
            "description": "Free allowances and their windows."
          },
          "paid": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentPrice"
            },
            "description": "List prices in USD. The operation's 402 is the payable quote."
          },
          "how_to_pay": {
            "type": "string",
            "description": "Payment instructions and availability restrictions."
          },
          "live": {
            "type": "string",
            "description": "Authoritative product quota endpoint.",
            "nullable": true
          },
          "free_now": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "SKUs temporarily free despite their list price."
          },
          "trial": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PaymentTrial"
              }
            ],
            "description": "Registration trial, when offered."
          }
        },
        "required": [
          "free",
          "paid",
          "how_to_pay",
          "live"
        ]
      },
      "PaymentFree": {
        "type": "object",
        "properties": {
          "o_que": {
            "type": "string",
            "description": "Operation or allowance."
          },
          "limite": {
            "type": "string",
            "description": "Allowance and eligibility."
          },
          "janela": {
            "type": "string",
            "description": "Reset window, when applicable.",
            "nullable": true
          }
        },
        "required": [
          "o_que",
          "limite",
          "janela"
        ]
      },
      "PaymentPrice": {
        "type": "object",
        "properties": {
          "o_que": {
            "type": "string",
            "description": "Operation and billing unit."
          },
          "price_usd": {
            "type": "number",
            "description": "Current list price in USD."
          }
        },
        "required": [
          "o_que",
          "price_usd"
        ]
      },
      "PaymentTrial": {
        "type": "object",
        "properties": {
          "days": {
            "type": "integer",
            "description": "Trial duration in days."
          },
          "how": {
            "type": "string",
            "description": "Eligibility and activation steps."
          }
        },
        "required": [
          "days",
          "how"
        ]
      }
    }
  },
  "paths": {
    "/okf/{arquivo}": {
      "get": {
        "operationId": "get_okf_by_arquivo",
        "summary": "Bundle OKF (Open Knowledge Format v0.1): markdown com frontmatter para o agente ler o produto inteiro sem parsear HTML.",
        "description": "Devolve: `text/markdown`. Comece por `/okf/index.md`, que lista o bundle.",
        "security": [],
        "parameters": [
          {
            "name": "arquivo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`index.md`, `sobre.md`, `api.md` ou `faq.md`.",
            "example": "index.md"
          }
        ],
        "responses": {
          "200": {
            "description": "`text/markdown`. Comece por `/okf/index.md`, que lista o bundle."
          },
          "404": {
            "description": "Arquivo fora do bundle."
          }
        }
      }
    },
    "/.well-known/{arquivo}": {
      "get": {
        "operationId": "get_well_known_by_arquivo",
        "summary": "Descoberta de máquina antes da home: `api-catalog` (RFC 9727, linkset com a API e o MCP), `security.txt` (RFC 9116), `x402` (manifesto de pagamento: rede, carteira e rotas que cobram), `agent-card.json` (identidade do agente: ferramentas MCP e portas de descoberta; também em `/agent.json`) e `mcp-registry-auth` (chave do registro oficial de MCP).",
        "description": "Devolve: `application/linkset+json` no api-catalog; `application/json` no x402, no agent-card.json e no apis.json; `text/plain` nos outros dois.",
        "security": [],
        "parameters": [
          {
            "name": "arquivo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`api-catalog`, `security.txt`, `x402`, `agent-card.json`, `mcp-registry-auth` ou `apis.json`.",
            "example": "api-catalog"
          }
        ],
        "responses": {
          "200": {
            "description": "`application/linkset+json` no api-catalog; `application/json` no x402, no agent-card.json e no apis.json; `text/plain` nos outros dois."
          },
          "404": {
            "description": "Nome fora dos seis publicados."
          }
        }
      }
    },
    "/apis.json": {
      "get": {
        "operationId": "get_apis_json",
        "summary": "APIs.json (apisjson.org, 0.19): o índice que o APIs.io colhe — a API, o MCP, OpenAPI, guia e bundle OKF num arquivo só. Também em `/.well-known/apis.json`.",
        "description": "Devolve: `application/json` no formato APIs.json 0.19: `apis[]` com `baseURL`, `humanURL` e `properties[]`.",
        "security": [],
        "responses": {
          "200": {
            "description": "`application/json` no formato APIs.json 0.19: `apis[]` com `baseURL`, `humanURL` e `properties[]`."
          }
        }
      }
    },
    "/agent.json": {
      "get": {
        "operationId": "get_agent_json",
        "summary": "Cartão do agente: identidade, quem opera, documentação, o endpoint MCP e as ferramentas que ele serve. Mesmo documento de `/.well-known/agent-card.json`.",
        "description": "Devolve: `application/json`: `name`, `provider`, `protocol` (`mcp`), `interfaces[]` e `skills[]`.",
        "security": [],
        "responses": {
          "200": {
            "description": "`application/json`: `name`, `provider`, `protocol` (`mcp`), `interfaces[]` e `skills[]`."
          }
        }
      }
    },
    "/api/": {
      "get": {
        "operationId": "api_index",
        "summary": "Índice auto-descrito da API inteira, com os idiomas e as páginas HTML de cada um.",
        "description": "Devolve: { name, description, locales, auth, docs, endpoints, quota, mcp, quickstart }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ name, description, locales, auth, docs, endpoints, quota, mcp, quickstart }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "description": "Nome do produto."
                    },
                    "description": {
                      "type": "string",
                      "description": "O que o produto faz, em uma frase."
                    },
                    "locales": {
                      "type": "object",
                      "description": "Idiomas atendidos e o caminho de cada página em cada um."
                    },
                    "auth": {
                      "type": "object",
                      "description": "Cada modo de autenticação e como obtê-lo."
                    },
                    "docs": {
                      "type": "object",
                      "description": "Links para llms.txt, llms-full.txt, openapi.json, MCP e a UI."
                    },
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Todo endpoint com método, caminho, auth, URL absoluta e o que devolve."
                    },
                    "quota": {
                      "type": "object",
                      "description": "O que é grátis, o que custa e como pagar — antes de você gastar chamada."
                    },
                    "mcp": {
                      "type": "object",
                      "description": "Endereço e transporte do servidor MCP."
                    },
                    "quickstart": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "As quatro chamadas que levam do zero à biblioteca."
                    }
                  },
                  "required": [
                    "name",
                    "description",
                    "locales",
                    "auth",
                    "docs",
                    "endpoints",
                    "quota",
                    "mcp",
                    "quickstart"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "health",
        "summary": "Liveness e o commit publicado agora — é como o smoke espera o próprio deploy.",
        "description": "Devolve: { ok, app, build, ts, catalog{synced_at,age_hours,stale,limit_days}, sources }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ ok, app, build, ts, catalog{synced_at,age_hours,stale,limit_days}, sources }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Saude"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "post_mcp",
        "summary": "Servidor MCP por HTTP (Streamable HTTP, JSON-RPC 2.0) — pluga no cliente sem instalar nada.",
        "description": "As tools são as operações deste mesmo catálogo; o MCP não tem backend próprio. `GET /mcp` devolve o cartão do servidor.\nDevolve: Resposta JSON-RPC 2.0 (`initialize`, `tools/list` ou `tools/call`).\nCredencial vai nos headers de sempre (X-Guest-Token, Authorization, X-PAYMENT) e é repassada à API.\nCota estourada chega como 402 com accepts[] dentro do resultado da tool — pague e repita.",
        "security": [],
        "responses": {
          "200": {
            "description": "Resposta JSON-RPC 2.0 (`initialize`, `tools/list` ou `tools/call`)."
          }
        }
      }
    },
    "/api/channels": {
      "get": {
        "operationId": "search_channels",
        "summary": "Busca paginada do catálogo público, com as facetas de categoria da busca atual.",
        "description": "É a porta de entrada do produto. A resposta varia por navegador, sistema e país de quem pede — cada canal traz `social.your_fails`, o recorte do SEU ambiente — por isso ela é `Cache-Control: private`.\nDevolve: { items[{id,name,alt_names,country,categories,category_labels,languages,language_labels,logo_url,website,playable_hint?,slug,network,owners,launched,replaced_by,feed_name,feed_format,timezones,broadcast_area,quality?,has_guide,guide_site,guide_lang,subdivision,city,kind,radio?,origem,guide_now?,health_ext,social?,api}], total, limit, offset, next_offset, facets{categories}, filters{q,country,category,language,network,quality,guide,subdivision,city,playable,sort,online,kind,tag} }",
        "security": [],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Texto livre no nome e nos apelidos do canal (busca full-text).",
            "example": "globo"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "País do canal, ISO 3166-1 alpha-2.",
            "example": "BR"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "ID de categoria do catálogo.",
            "example": "news"
          },
          {
            "name": "language",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Idioma do canal, ISO 639-3.",
            "example": "por"
          },
          {
            "name": "network",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Nome exato da rede/emissora.",
            "example": "Globo"
          },
          {
            "name": "quality",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Qualidade exata do stream.",
            "example": "1080p"
          },
          {
            "name": "guide",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": "0",
              "enum": [
                "0",
                "1"
              ]
            },
            "description": "`1` traz só canal com grade de programação (EPG)."
          },
          {
            "name": "subdivision",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Estado/província, código do catálogo.",
            "example": "BR-SP"
          },
          {
            "name": "city",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Cidade, código do catálogo."
          },
          {
            "name": "playable",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": "1",
              "enum": [
                "0",
                "1"
              ]
            },
            "description": "`0` inclui canal sem stream utilizável conhecido."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "name",
              "enum": [
                "name",
                "score",
                "votes"
              ]
            },
            "description": "`score` ordena pela saúde medida por terceiro, melhor primeiro; canal não medido vai para o fim. `votes` ordena pelos votos registrados para a estação (rádio). `name` é a ordem alfabética."
          },
          {
            "name": "online",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": "0",
              "enum": [
                "0",
                "1"
              ]
            },
            "description": "`1` traz só canal visto online pela fonte nas 48 h anteriores à última recarga do catálogo (`health_ext.online`); com o catálogo parado há mais de 48 h o filtro não devolve ninguém."
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "tv",
              "enum": [
                "tv",
                "radio",
                "all"
              ]
            },
            "description": "`tv` (padrão) é o catálogo de TV; `radio` são as estações de rádio; `all` junta os dois. Sem `kind`, rádio nunca aparece."
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tag da estação de rádio (vocabulário livre, ex. `mpb`, `news`); veja `GET /api/tags`.",
            "example": "mpb"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Itens por página. Acima de 50 é silenciosamente reduzido a 50."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Quantos itens pular. Use `next_offset` da resposta anterior."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{id,name,alt_names,country,categories,category_labels,languages,language_labels,logo_url,website,playable_hint?,slug,network,owners,launched,replaced_by,feed_name,feed_format,timezones,broadcast_area,quality?,has_guide,guide_site,guide_lang,subdivision,city,kind,radio?,origem,guide_now?,health_ext,social?,api}], total, limit, offset, next_offset, facets{categories}, filters{q,country,category,language,network,quality,guide,subdivision,city,playable,sort,online,kind,tag} }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginaDeCanais"
                }
              }
            }
          }
        }
      }
    },
    "/api/channels/{id}": {
      "get": {
        "operationId": "get_channel",
        "summary": "Ficha completa de um canal, com os streams já apontando para o nosso hop.",
        "description": "Devolve: { id, name, alt_names, country, categories, category_labels, languages, language_labels, logo_url, website, playable_hint?, slug, network, owners, launched, replaced_by, feed_name, feed_format, timezones, broadcast_area, quality?, has_guide, guide_site, guide_lang, subdivision, city, kind, radio?{tags,votes,clicks,codec,bitrate,geo}, origem, guide_now?{day,site,agora,a_seguir}, health_ext{score,online,checked_at}, social?{plays,fails,favorites,comments,last_fail_code,health,your_plays,your_fails,your_fail_code,your_country_ok,your_country_fail,your_geo_ok,your_latency_ms,your_latency_grade}, api, streams[{id,provider_url,legacy_url,feed,title,url,quality,needs_headers,label,scheme,kind,youtube?,playable_hint,origem,health_ext}], _links{self,app,api_index} }",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do canal no catálogo, ex. `GloboNews.br`.",
            "example": "GloboNews.br"
          }
        ],
        "responses": {
          "200": {
            "description": "{ id, name, alt_names, country, categories, category_labels, languages, language_labels, logo_url, website, playable_hint?, slug, network, owners, launched, replaced_by, feed_name, feed_format, timezones, broadcast_area, quality?, has_guide, guide_site, guide_lang, subdivision, city, kind, radio?{tags,votes,clicks,codec,bitrate,geo}, origem, guide_now?{day,site,agora,a_seguir}, health_ext{score,online,checked_at}, social?{plays,fails,favorites,comments,last_fail_code,health,your_plays,your_fails,your_fail_code,your_country_ok,your_country_fail,your_geo_ok,your_latency_ms,your_latency_grade}, api, streams[{id,provider_url,legacy_url,feed,title,url,quality,needs_headers,label,scheme,kind,youtube?,playable_hint,origem,health_ext}], _links{self,app,api_index} }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CanalCompleto"
                }
              }
            }
          },
          "404": {
            "description": "Canal não disponível no catálogo."
          }
        }
      }
    },
    "/api/channels/{id}/health": {
      "get": {
        "operationId": "channel_health",
        "summary": "Por que o canal falha, para quem e onde — inclui geo-bloqueio, latência por região e o veredito de quem está chamando.",
        "description": "É o que separa 'o canal está fora do ar' de 'o canal está bloqueado no seu país'. `geo` diz se é restrição regional ou falha geral (com os países), `regions[]` traz sucesso/falha por país, `latency[]` a velocidade de abertura medida no hop por país, e `pra_voce` resume tudo para o país de quem chama. Sem relato da comunidade (`POST /api/play-report`) o painel fica vazio.\nDevolve: { channel_id, plays, fails, favorites, comments, health, last_ok_at, last_fail_at, last_fail_code, min_relatos, reasons[{code,label,hint,count,last_at}], environments[{browser,os,country,label,plays,fails,health,last_fail_code,last_at}], your_environment{browser,os,country,label,plays,fails,health,last_fail_code,last_at}, regions[{country,plays,fails,health,latency_ms,latency_grade}], geo{tipo,bloqueado_em,funciona_em,label}, latency[{country,samples,avg_ms,grade,grade_label,last_at}], your_country{country,plays,fails,health,latency_ms,latency_grade}, pra_voce, codes, _links{self,channel,report} }",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do canal no catálogo.",
            "example": "GloboNews.br"
          }
        ],
        "responses": {
          "200": {
            "description": "{ channel_id, plays, fails, favorites, comments, health, last_ok_at, last_fail_at, last_fail_code, min_relatos, reasons[{code,label,hint,count,last_at}], environments[{browser,os,country,label,plays,fails,health,last_fail_code,last_at}], your_environment{browser,os,country,label,plays,fails,health,last_fail_code,last_at}, regions[{country,plays,fails,health,latency_ms,latency_grade}], geo{tipo,bloqueado_em,funciona_em,label}, latency[{country,samples,avg_ms,grade,grade_label,last_at}], your_country{country,plays,fails,health,latency_ms,latency_grade}, pra_voce, codes, _links{self,channel,report} }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SaudeCanal"
                }
              }
            }
          },
          "404": {
            "description": "Canal não existe no catálogo."
          }
        }
      }
    },
    "/api/channels/{id}/guia": {
      "get": {
        "operationId": "get_channel_guide",
        "summary": "Programação de hoje do canal, grabada por nós: o que está no ar agora e o que vem a seguir.",
        "description": "Disponível nos canais com guia (`guide=1`), com validade de até dois dias. Confira a data da resposta; a ficha traz o resumo em `guide_now`.\nDevolve: { channel_id, day, site, agora{inicio,fim,titulo,desc?,categoria?}, a_seguir{inicio,fim,titulo,desc?,categoria?}, programas[{inicio,fim,titulo,desc?,categoria?}] }",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do canal no catálogo.",
            "example": "RecordNews.br"
          }
        ],
        "responses": {
          "200": {
            "description": "{ channel_id, day, site, agora{inicio,fim,titulo,desc?,categoria?}, a_seguir{inicio,fim,titulo,desc?,categoria?}, programas[{inicio,fim,titulo,desc?,categoria?}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GuiaDoDia"
                }
              }
            }
          },
          "404": {
            "description": "Canal sem guia do dia (ou com guia velha)."
          }
        }
      }
    },
    "/api/geo": {
      "get": {
        "operationId": "geo",
        "summary": "País e idioma sugeridos para quem está chamando.",
        "description": "Devolve: { country, detected, language, language_detected, source, api }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ country, detected, language, language_detected, source, api }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Geo"
                }
              }
            }
          }
        }
      }
    },
    "/api/countries": {
      "get": {
        "operationId": "list_countries",
        "summary": "Países que têm canal tocável, com a contagem e a bandeira de cada um.",
        "description": "Devolve: { items[{code,name,count,flag}] }",
        "security": [],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "tv",
              "enum": [
                "tv",
                "radio",
                "all"
              ]
            },
            "description": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{code,name,count,flag}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaPais"
                }
              }
            }
          }
        }
      }
    },
    "/api/tags": {
      "get": {
        "operationId": "list_tags",
        "summary": "Tags das estações de rádio tocáveis (vocabulário livre), com a contagem de cada uma.",
        "description": "Devolve: { items[{id,name,count}] }",
        "security": [],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restringe às estações de um país, ISO 3166-1 alpha-2.",
            "example": "BR"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 40
            },
            "description": "Quantas tags devolver (teto 100)."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{id,name,count}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaTag"
                }
              }
            }
          }
        }
      }
    },
    "/api/categories": {
      "get": {
        "operationId": "get_api_categories",
        "summary": "Vocabulário de categorias do catálogo, com ícone para a interface.",
        "description": "Note que `POST /api/categories` é outra coisa: cria pasta na biblioteca do dono.\nDevolve: { items[{id,name,description,icon}] }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ items[{id,name,description,icon}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaCategoria"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "create_category",
        "summary": "Cria uma pasta na galeria, já com a sub-aba Geral dentro dela.",
        "description": "Teto de 8 pastas por dono. A resposta traz a biblioteca inteira já atualizada — não precisa recarregar `GET /api/library` depois.\nDevolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, name, slug, group_id }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Nome da pasta, até 40 caracteres."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Notícias"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, name, slug, group_id }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BibliotecaComPasta"
                }
              }
            }
          },
          "400": {
            "description": "Nome vazio, longo demais, ou o teto de 8 pastas foi atingido."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      }
    },
    "/api/languages": {
      "get": {
        "operationId": "list_languages",
        "summary": "Idiomas que têm canal tocável, com a contagem de cada um.",
        "description": "Devolve: { items[{code,name,count}] }",
        "security": [],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "tv",
              "enum": [
                "tv",
                "radio",
                "all"
              ]
            },
            "description": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{code,name,count}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaIdioma"
                }
              }
            }
          }
        }
      }
    },
    "/api/networks": {
      "get": {
        "operationId": "list_networks",
        "summary": "Redes e emissoras que têm canal tocável, com a contagem.",
        "description": "Devolve: { items[{name,count}] }",
        "security": [],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "tv",
              "enum": [
                "tv",
                "radio",
                "all"
              ]
            },
            "description": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{name,count}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaRede"
                }
              }
            }
          }
        }
      }
    },
    "/api/qualities": {
      "get": {
        "operationId": "list_qualities",
        "summary": "Qualidades distintas encontradas nos streams do catálogo (em rádio, codec e bitrate).",
        "description": "Devolve: { items[{id,name,count}] }",
        "security": [],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "tv",
              "enum": [
                "tv",
                "radio",
                "all"
              ]
            },
            "description": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{id,name,count}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaQualidade"
                }
              }
            }
          }
        }
      }
    },
    "/api/subdivisions": {
      "get": {
        "operationId": "list_subdivisions",
        "summary": "Estados e províncias que têm canal tocável.",
        "description": "Devolve: { items[{code,country,name,count}] }",
        "security": [],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restringe a um país, ISO 3166-1 alpha-2.",
            "example": "BR"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "tv",
              "enum": [
                "tv",
                "radio",
                "all"
              ]
            },
            "description": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{code,country,name,count}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaSubdivisao"
                }
              }
            }
          }
        }
      }
    },
    "/api/cities": {
      "get": {
        "operationId": "list_cities",
        "summary": "Cidades que têm canal tocável, filtráveis por país e por estado.",
        "description": "Devolve: { items[{code,country,subdivision,name,count}] }",
        "security": [],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restringe a um país, ISO 3166-1 alpha-2.",
            "example": "BR"
          },
          {
            "name": "subdivision",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restringe a um estado/província.",
            "example": "BR-SP"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "tv",
              "enum": [
                "tv",
                "radio",
                "all"
              ]
            },
            "description": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{code,country,subdivision,name,count}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaCidade"
                }
              }
            }
          }
        }
      }
    },
    "/api/producers": {
      "get": {
        "operationId": "producer_services",
        "summary": "Oferta sob consulta para produtores com conteúdo autorizado e canais de contato.",
        "description": "Somente informações e captação de interesse. Não provisiona, não cobra e não ativa transmissão. Use contact.form_url no browser, contact.email por e-mail ou POST /api/contact para apresentar o projeto. O envio por API mantém o gate x402 ou crédito pré-pago; o formulário humano usa Turnstile.\nDevolve: { status, activation_available, title, description, audience, services, requirements, availability, pricing, contact, _links }",
        "security": [],
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "pt",
              "enum": [
                "pt",
                "en",
                "es",
                "fr",
                "de"
              ]
            },
            "description": "Idioma da oferta: pt, en, es, fr ou de; ausente ou desconhecido volta a pt."
          }
        ],
        "responses": {
          "200": {
            "description": "{ status, activation_available, title, description, audience, services, requirements, availability, pricing, contact, _links }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "description": "`sob_consulta`: proposta sujeita a avaliação individual."
                    },
                    "activation_available": {
                      "type": "boolean",
                      "description": "Sempre false: não há ativação de transmissão nesta superfície."
                    },
                    "title": {
                      "type": "string",
                      "description": "Nome da oferta no idioma solicitado."
                    },
                    "description": {
                      "type": "string",
                      "description": "Apresentação do serviço sob consulta."
                    },
                    "audience": {
                      "type": "string",
                      "description": "Perfil de produtores e organizações atendidos pela proposta."
                    },
                    "services": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Capacidades a avaliar no projeto, sem compromisso de disponibilidade."
                    },
                    "requirements": {
                      "type": "string",
                      "description": "Necessidade de autorização para sinal e obras, território e prazo."
                    },
                    "availability": {
                      "type": "string",
                      "description": "Condição de avaliação antes de confirmar início e escopo."
                    },
                    "pricing": {
                      "type": "string",
                      "description": "Orçamento sob consulta; enviar interesse não contrata o serviço."
                    },
                    "contact": {
                      "type": "object",
                      "description": "email, form_url e api_url absolutos, message_template e instructions para apresentar canal/evento, direitos, audiência, duração e data."
                    },
                    "_links": {
                      "type": "object",
                      "description": "self (esta API com idioma) e page (página da oferta): URLs absolutas."
                    }
                  },
                  "required": [
                    "status",
                    "activation_available",
                    "title",
                    "description",
                    "audience",
                    "services",
                    "requirements",
                    "availability",
                    "pricing",
                    "contact",
                    "_links"
                  ]
                }
              }
            }
          },
          "405": {
            "description": "A oferta só aceita GET; não existe provisionamento por POST."
          }
        }
      }
    },
    "/logos/{id}": {
      "get": {
        "operationId": "get_logos_by_id",
        "summary": "Logo do canal servido por nós, na variante de card (≤256px).",
        "description": "Nunca faz proxy na hora: ou o arquivo está no R2, ou responde 404. É o que impede a página de card virar um proxy de imagem de terceiro.\nDevolve: `image/webp` ou `image/png` — os bytes do logo.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do logo, que vem em `Canal.logo_url`.",
            "example": "GloboNews.br"
          }
        ],
        "responses": {
          "200": {
            "description": "`image/webp` ou `image/png` — os bytes do logo."
          },
          "404": {
            "description": "Não há variante em cache para este canal."
          }
        }
      }
    },
    "/api/s/{id}": {
      "get": {
        "operationId": "get_api_s_by_id",
        "summary": "Hop do stream: a URL pública aponta para https://m3m8.gradetv.net; o apex ainda 302 para compatibilidade. Mídia continua direta. Conta o play uma vez por pessoa, canal e dia.",
        "description": "É o endereço que aparece no M3U e na API: https://m3m8.gradetv.net/api/s/:id, com CORS * e cache de 1 s na CDN (browser continua no-store). Links antigos em gradetv.net/api/s/ ainda 302 no-store para o mesmo path. O Grade NÃO retransmite vídeo: os segmentos e chaves vêm da origem, no browser e no VLC. Playlists internas usam o mesmo hop com `p` assinado; copie a URL completa, sem montar o ticket. Cada busca tem teto de 512 KiB, 8 s e 3 redirects revalidados; até 128 referências internas por manifesto e 4 níveis. Refresh interno não conta outro play. Falha de playlist no o2 é 422 JSON (a CDN não reescreve 4xx); o Worker de fallback ainda usa 502. Sem redirecionamento para HTTP. Headers expostos por CORS: X-Grade-Upstream-Status (status observado na origem), X-Grade-Source (URL original da transmissão para copiar), X-Grade-Website (site oficial do canal, vazio quando ausente), X-Grade-Legacy (ação HTTP pública) e X-Grade-Media-HTTP (1 se este manifesto referencia mídia/chave HTTP, 0 caso contrário). Mídia HTTP oferece player legado isolado; 401/403 da origem oferece abrir o site do canal e copiar a playlist original para outro player. CORS ainda pode impedir reprodução. Áudio/vídeo contínuo e YouTube recebem 302 para a origem. IDs novos não dependem da ordem da recarga. Um ID posicional antigo ausente recupera um stream do mesmo canal, feed e fonte, preservando os bloqueios.\nDevolve: A playlist é `application/vnd.apple.mpegurl` em m3m8.gradetv.net, com playlists internas pelo hop e mídia com URI absoluta da origem. Pedido legado no apex pode 302 para esse host. Stream contínuo/YouTube recebe 302 para o provedor.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do stream, que vem em `Stream.id`.",
            "example": "1001Noites.br:SD:2168"
          },
          {
            "name": "p",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Ticket de playlist interna emitido pelo hop, até 6000 caracteres, vinculado ao stream e à origem. Permanece válido enquanto origem/bloqueio permitirem."
          }
        ],
        "responses": {
          "200": {
            "description": "A playlist é `application/vnd.apple.mpegurl` em m3m8.gradetv.net, com playlists internas pelo hop e mídia com URI absoluta da origem. Pedido legado no apex pode 302 para esse host. Stream contínuo/YouTube recebe 302 para o provedor."
          },
          "404": {
            "description": "Stream bloqueado/ausente, ticket `p` inválido/expirado ou origem alterada."
          },
          "422": {
            "description": "`playlist_acesso`/`playlist_origem`/`playlist_limite` no hop do o2 (a CDN não reescreve 4xx)."
          },
          "502": {
            "description": "`playlist_acesso` para 401/403 da origem (`upstream_status` no corpo); `playlist_origem` para outras falhas; `playlist_limite` para limite excedido."
          },
          "503": {
            "description": "Origem de playlists indisponível ou limite operacional atingido; `playlist_config` indica chave de assinatura indisponível."
          }
        }
      }
    },
    "/api/legacy/{id}": {
      "get": {
        "operationId": "legacy_stream",
        "summary": "Metadados públicos para o player legado HTTP, sem sessão.",
        "description": "O botão legacy_url abre http://legacy.gradetv.net:8080/legacy?stream=ID. A página isolada aceita lang=pt/en/es/fr/de e theme=light/dark. Playlists continuam pelo hop HTTPS e mídia direto da origem. Não remove recusa de acesso nem exigência CORS. Uma leitura indexada; não busca origem nem grava no D1.\nDevolve: { id, name, kind, radio, url, provider_url, website, legacy_url }",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID público do stream, incluindo compatibilidade de IDs antigos.",
            "example": "1001Noites.br:SD:2168"
          }
        ],
        "responses": {
          "200": {
            "description": "{ id, name, kind, radio, url, provider_url, website, legacy_url }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "ID resolvido do stream."
                    },
                    "name": {
                      "type": "string",
                      "description": "Nome do canal."
                    },
                    "kind": {
                      "type": "string",
                      "description": "Tipo do stream para o player."
                    },
                    "radio": {
                      "type": "boolean",
                      "description": "Se o canal é rádio."
                    },
                    "url": {
                      "type": "string",
                      "description": "URL pública do hop HTTPS de playlist."
                    },
                    "provider_url": {
                      "type": "string",
                      "description": "URL direta da transmissão no provedor."
                    },
                    "website": {
                      "type": "string",
                      "description": "Site oficial do canal; null quando ausente ou inválido. Destino do botão de abrir site, separado da URL da transmissão.",
                      "nullable": true
                    },
                    "legacy_url": {
                      "type": "string",
                      "description": "Player HTTP isolado, sem credencial."
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "kind",
                    "radio",
                    "url",
                    "provider_url",
                    "website",
                    "legacy_url"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Stream indisponível ou origem incompatível."
          }
        }
      }
    },
    "/api/m/{ticket}": {
      "get": {
        "operationId": "get_api_m_by_ticket",
        "summary": "Desligado: era o pass-through de vídeo. Responde 410 sempre.",
        "description": "Até 05/09/2026 encaminhava os bytes da origem com CORS nosso. Relay de stream de terceiro não é serviço do Grade; a rota fica documentada para quem ainda tiver o endereço numa playlist antiga.\nDevolve: 410 `relay_desligado`, sempre.",
        "security": [],
        "parameters": [
          {
            "name": "ticket",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ticket legado ignorado; nenhum valor reativa a retransmissão.",
            "example": "tk_legado"
          }
        ],
        "responses": {
          "200": {
            "description": "410 `relay_desligado`, sempre."
          },
          "404": {
            "description": "Caminho inexistente fora da família retirada."
          },
          "410": {
            "description": "Sempre: o Grade não retransmite vídeo."
          }
        }
      }
    },
    "/f/{token}/library.{formato}": {
      "get": {
        "operationId": "get_f_by_token_library_formato",
        "summary": "Feed da biblioteca inteira do dono, no formato pedido pela extensão.",
        "description": "É a URL que a pessoa cola no VLC. Não pede credencial: o token no caminho É a credencial, e quem tem o link tem o conteúdo — trate como segredo. As URLs saem prontas em `Biblioteca.feeds`, e cada pasta e sub-aba tem a sua. Itens com IDs posicionais antigos se recuperam na leitura, enquanto o mesmo canal, feed e fonte continuarem disponíveis; não é preciso salvar novamente.\nDevolve: `audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`.\nItem do YouTube fica fora do M3U e do XSPF (o VLC não toca) e vem no JSON com `kind: \"youtube\"`.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token de feed do dono; vem em `Biblioteca.feeds` e não é o guest token.",
            "example": "k7m2p9r4t6v8w1y3z5b7c9d1"
          },
          {
            "name": "formato",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "m3u",
                "m3u8",
                "json",
                "xspf"
              ]
            },
            "description": "Extensão que escolhe o formato de saída.",
            "example": "m3u"
          }
        ],
        "responses": {
          "200": {
            "description": "`audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`."
          },
          "404": {
            "description": "Token de feed desconhecido."
          }
        }
      }
    },
    "/f/{token}/c/{categoria}.{formato}": {
      "get": {
        "operationId": "get_f_by_token_c_by_categoria_formato",
        "summary": "Feed de uma pasta da biblioteca, para assinar só aquele recorte.",
        "description": "Devolve: `audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`.\nItem do YouTube fica fora do M3U e do XSPF (o VLC não toca) e vem no JSON com `kind: \"youtube\"`.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token de feed do dono, vindo de `Biblioteca.feeds`.",
            "example": "k7m2p9r4t6v8w1y3z5b7c9d1"
          },
          {
            "name": "categoria",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Slug da pasta, que vem em `PastaBiblioteca.slug`.",
            "example": "jornalismo"
          },
          {
            "name": "formato",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "m3u",
                "m3u8",
                "json",
                "xspf"
              ]
            },
            "description": "Extensão que escolhe o formato de saída.",
            "example": "m3u"
          }
        ],
        "responses": {
          "200": {
            "description": "`audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`."
          },
          "404": {
            "description": "Token de feed ou pasta desconhecidos."
          }
        }
      }
    },
    "/f/{token}/c/{categoria}/g/{grupo}.{formato}": {
      "get": {
        "operationId": "get_f_by_token_c_by_categoria_g_by_grupo_formato",
        "summary": "Feed de uma sub-aba — o recorte mais fino que a galeria oferece.",
        "description": "Devolve: `audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`.\nItem do YouTube fica fora do M3U e do XSPF (o VLC não toca) e vem no JSON com `kind: \"youtube\"`.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token de feed do dono, vindo de `Biblioteca.feeds`.",
            "example": "k7m2p9r4t6v8w1y3z5b7c9d1"
          },
          {
            "name": "categoria",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Slug da pasta que contém a sub-aba.",
            "example": "noticias"
          },
          {
            "name": "grupo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Slug da sub-aba, que vem em `SubAba.slug`.",
            "example": "manchete"
          },
          {
            "name": "formato",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "m3u",
                "m3u8",
                "json",
                "xspf"
              ]
            },
            "description": "Extensão que escolhe o formato de saída.",
            "example": "m3u"
          }
        ],
        "responses": {
          "200": {
            "description": "`audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`."
          },
          "404": {
            "description": "Token de feed, pasta ou sub-aba desconhecidos."
          }
        }
      }
    },
    "/api/guest": {
      "post": {
        "operationId": "create_guest",
        "summary": "Cria um convidado `ipt_…` — é a identidade que guarda galeria, histórico e favoritos sem conta.",
        "description": "Não pede e-mail nem nada. Guarde o token: perdeu o token, perdeu a biblioteca — a não ser que a conta já o tenha reivindicado (`POST /api/auth/claim`), e aí o que ele guardou está na conta.\nDevolve: { token }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ token }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "description": "O convidado, prefixo `ipt_`. Mande em `X-Guest-Token` ou como Bearer."
                    }
                  },
                  "required": [
                    "token"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/keys": {
      "post": {
        "operationId": "post_api_keys",
        "summary": "Retirada: criava chave `iptk_…`. Responde 410.",
        "description": "Conta e pagamento são globais desde 21/09/2026 e o SDK não tem chave de API por conta. Identifique-se com `X-Guest-Token: ipt_…` (`POST /api/guest`) ou pela sessão da conta; chamada paga usa o crédito global (`Authorization: Bearer cred_…`) ou x402.\nDevolve: 410 `api_keys_retired`, sempre.",
        "security": [],
        "responses": {
          "200": {
            "description": "410 `api_keys_retired`, sempre."
          },
          "410": {
            "description": "Sempre: as chaves de API próprias foram retiradas."
          }
        }
      },
      "get": {
        "operationId": "get_api_keys",
        "summary": "Retirada: listava as chaves `iptk_…`. Responde 410.",
        "description": "Conta e pagamento são globais desde 21/09/2026 e o SDK não tem chave de API por conta. Identifique-se com `X-Guest-Token: ipt_…` (`POST /api/guest`) ou pela sessão da conta; chamada paga usa o crédito global (`Authorization: Bearer cred_…`) ou x402.\nDevolve: 410 `api_keys_retired`, sempre.",
        "security": [],
        "responses": {
          "200": {
            "description": "410 `api_keys_retired`, sempre."
          },
          "410": {
            "description": "Sempre: as chaves de API próprias foram retiradas."
          }
        }
      }
    },
    "/api/keys/{id}": {
      "delete": {
        "operationId": "delete_api_keys_by_id",
        "summary": "Retirada: revogava uma chave `iptk_…`. Responde 410.",
        "description": "Conta e pagamento são globais desde 21/09/2026 e o SDK não tem chave de API por conta. Identifique-se com `X-Guest-Token: ipt_…` (`POST /api/guest`) ou pela sessão da conta; chamada paga usa o crédito global (`Authorization: Bearer cred_…`) ou x402.\nDevolve: 410 `api_keys_retired`, sempre.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID de chave antiga; ignorado.",
            "example": "key_9f3c2b1d7a"
          }
        ],
        "responses": {
          "200": {
            "description": "410 `api_keys_retired`, sempre."
          },
          "404": {
            "description": "Caminho fora da família retirada — todo `/api/keys/…` responde 410."
          },
          "410": {
            "description": "Sempre: as chaves de API próprias foram retiradas."
          }
        }
      }
    },
    "/api/library": {
      "get": {
        "operationId": "get_library",
        "summary": "A galeria inteira do dono: pastas, sub-abas, canais e as URLs de feed de cada nível.",
        "description": "Devolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Biblioteca"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      }
    },
    "/api/categories/{id}": {
      "patch": {
        "operationId": "patch_api_categories_by_id",
        "summary": "Renomeia uma pasta. O slug do feed acompanha o nome novo.",
        "description": "Atenção: mudar o nome muda o slug, e portanto muda a URL do feed daquela pasta. Quem já tinha colado o link antigo no player precisa colar o novo.\nDevolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID da pasta, `cat_…`.",
            "example": "cat_123"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Novo nome da pasta, até 40 caracteres."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Jornalismo"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Biblioteca"
                }
              }
            }
          },
          "400": {
            "description": "Nome vazio ou longo demais."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Recurso não existe (ou não é seu — a API não distingue os dois de propósito)."
          }
        }
      },
      "delete": {
        "operationId": "delete_api_categories_by_id",
        "summary": "Apaga a pasta e tudo que está dentro dela: sub-abas e canais.",
        "description": "Devolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID da pasta, `cat_…`.",
            "example": "cat_123"
          }
        ],
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Biblioteca"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Recurso não existe (ou não é seu — a API não distingue os dois de propósito)."
          }
        }
      }
    },
    "/api/groups": {
      "post": {
        "operationId": "create_group",
        "summary": "Cria uma sub-aba dentro de uma pasta.",
        "description": "Teto de 12 sub-abas por pasta. A resposta traz a biblioteca inteira já atualizada — não precisa recarregar `GET /api/library` depois.\nDevolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, name, slug, category_id }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "category_id": {
                    "type": "string",
                    "description": "Pasta que vai receber a sub-aba, `cat_…`."
                  },
                  "name": {
                    "type": "string",
                    "description": "Nome da sub-aba, até 40 caracteres."
                  }
                },
                "required": [
                  "category_id",
                  "name"
                ]
              },
              "example": {
                "category_id": "cat_…",
                "name": "Manchete"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, name, slug, category_id }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BibliotecaComSubAba"
                }
              }
            }
          },
          "400": {
            "description": "Nome inválido ou teto de 12 sub-abas atingido."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "A pasta não é sua ou não existe."
          }
        }
      }
    },
    "/api/groups/{id}": {
      "delete": {
        "operationId": "delete_api_groups_by_id",
        "summary": "Apaga uma sub-aba e os canais que estavam nela.",
        "description": "Devolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID da sub-aba, `grp_…`.",
            "example": "grp_123"
          }
        ],
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Biblioteca"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Recurso não existe (ou não é seu — a API não distingue os dois de propósito)."
          }
        }
      }
    },
    "/api/items": {
      "post": {
        "operationId": "add_item",
        "summary": "Põe um canal do catálogo numa sub-aba da galeria.",
        "description": "Teto de 40 canais por sub-aba. A resposta diz em que pasta e sub-aba o canal caiu, para a tela abrir no lugar certo. A resposta traz a biblioteca inteira já atualizada — não precisa recarregar `GET /api/library` depois.\nDevolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, group_id, category_id }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "group_id": {
                    "type": "string",
                    "description": "Sub-aba que recebe o canal, `grp_…`."
                  },
                  "channel_id": {
                    "type": "string",
                    "description": "ID do canal no catálogo, ex. `GloboNews.br`."
                  },
                  "stream_id": {
                    "type": "string",
                    "description": "Stream específico; sem ele o servidor escolhe o melhor."
                  }
                },
                "required": [
                  "group_id",
                  "channel_id"
                ]
              },
              "example": {
                "group_id": "grp_…",
                "channel_id": "GloboNews.br"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, group_id, category_id }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BibliotecaComItem"
                }
              }
            }
          },
          "400": {
            "description": "Teto de 40 canais na sub-aba atingido."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Sub-aba ou canal não encontrados."
          }
        }
      }
    },
    "/api/items/{id}": {
      "delete": {
        "operationId": "delete_api_items_by_id",
        "summary": "Tira um canal da sub-aba. O canal continua no catálogo público, claro.",
        "description": "Devolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do item na galeria, `itm_…`.",
            "example": "itm_123"
          }
        ],
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Biblioteca"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Recurso não existe (ou não é seu — a API não distingue os dois de propósito)."
          }
        }
      }
    },
    "/api/items/{id}/play": {
      "post": {
        "operationId": "post_api_items_by_id_play",
        "summary": "Marca este canal como o último tocado da sub-aba — é o que devolve a pessoa onde parou.",
        "description": "Não conta play público nem entra no histórico; para isso são `POST /api/history` e `POST /api/play-report`.\nDevolve: { owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do item na galeria, `itm_…`.",
            "example": "itm_123"
          }
        ],
        "responses": {
          "200": {
            "description": "{ owner, feeds{m3u,m3u8,json,xspf}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Biblioteca"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Recurso não existe (ou não é seu — a API não distingue os dois de propósito)."
          }
        }
      }
    },
    "/api/history": {
      "get": {
        "operationId": "get_history",
        "summary": "Canais que o dono assistiu, do mais recente para o mais antigo.",
        "description": "Exibe somente canais disponíveis no catálogo.\nDevolve: { items[{channel_id,name,country,logo_url,playable_hint,plays,first_at,last_at,stale,api}], total, limit, offset, next_offset, next, api, max }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Itens por página. Acima de 50 é silenciosamente reduzido a 50."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Quantos itens pular. Use `next_offset` da resposta anterior."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{channel_id,name,country,logo_url,playable_hint,plays,first_at,last_at,stale,api}], total, limit, offset, next_offset, next, api, max }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginaDeHistorico"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      },
      "post": {
        "operationId": "record_watch",
        "summary": "Registra que o dono assistiu um canal. Repetir soma em `plays` e sobe a linha.",
        "description": "Devolve: { channel_id, plays, last_at, api }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel_id": {
                    "type": "string",
                    "description": "Canal assistido, ex. `GloboNews.br`."
                  }
                },
                "required": [
                  "channel_id"
                ]
              },
              "example": {
                "channel_id": "GloboNews.br"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ channel_id, plays, last_at, api }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "channel_id": {
                      "type": "string",
                      "description": "O canal registrado."
                    },
                    "plays": {
                      "type": "integer",
                      "description": "Quantas vezes o dono já assistiu este canal."
                    },
                    "last_at": {
                      "type": "string",
                      "description": "Momento deste registro (UTC).",
                      "nullable": true
                    },
                    "api": {
                      "type": "string",
                      "description": "URL absoluta do histórico."
                    }
                  },
                  "required": [
                    "channel_id",
                    "plays",
                    "last_at",
                    "api"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`channel_id` ausente ou JSON inválido."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      },
      "delete": {
        "operationId": "clear_history",
        "summary": "Limpa o histórico inteiro do dono, de uma vez.",
        "description": "Devolve: { ok, cleared, total }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "{ ok, cleared, total }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true`."
                    },
                    "cleared": {
                      "type": "boolean",
                      "description": "Sempre `true` — o histórico foi zerado."
                    },
                    "total": {
                      "type": "integer",
                      "description": "Quantos restaram: zero."
                    }
                  },
                  "required": [
                    "ok",
                    "cleared",
                    "total"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      }
    },
    "/api/history/{channel_id}": {
      "delete": {
        "operationId": "forget_watch",
        "summary": "Tira um canal do histórico do dono.",
        "description": "Devolve: { ok, removed }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "channel_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Canal a remover do histórico, ex. `GloboNews.br`.",
            "example": "GloboNews.br"
          }
        ],
        "responses": {
          "200": {
            "description": "{ ok, removed }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true` quando removeu."
                    },
                    "removed": {
                      "type": "string",
                      "description": "O `channel_id` que saiu."
                    }
                  },
                  "required": [
                    "ok",
                    "removed"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Recurso não existe (ou não é seu — a API não distingue os dois de propósito)."
          }
        }
      }
    },
    "/api/play-report": {
      "post": {
        "operationId": "report_play",
        "summary": "Relata se o canal tocou ou falhou — é o que alimenta a saúde pública do catálogo.",
        "description": "Conta uma vez por dono, por canal, por dia e por resultado; relatar de novo devolve 200 com `reason: ja_relatado_hoje`, e não é erro. Navegador e sistema saem do User-Agent e o país da borda — mandar isso no corpo não muda nada. Sem relato, o catálogo não aprende: é assim que `GET /api/channels/:id/health` sabe distinguir canal fora do ar de canal bloqueado para você.\nDevolve: { ok, counted, reason?, channel_id, stats{plays,fails,favorites,comments,last_fail_code,health,your_plays,your_fails,your_fail_code,your_country_ok,your_country_fail,your_geo_ok,your_latency_ms,your_latency_grade}, health }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel_id": {
                    "type": "string",
                    "description": "Canal que você tentou assistir."
                  },
                  "ok": {
                    "type": "boolean",
                    "description": "`true` se tocou, `false` se falhou."
                  },
                  "code": {
                    "type": "string",
                    "description": "Por que falhou; só quando `ok` é `false`."
                  }
                },
                "required": [
                  "channel_id",
                  "ok"
                ]
              },
              "example": {
                "channel_id": "GloboNews.br",
                "ok": false,
                "code": "cors"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, counted, reason?, channel_id, stats{plays,fails,favorites,comments,last_fail_code,health,your_plays,your_fails,your_fail_code,your_country_ok,your_country_fail,your_geo_ok,your_latency_ms,your_latency_grade}, health }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true` — o relato foi aceito."
                    },
                    "counted": {
                      "type": "boolean",
                      "description": "`false` quando você já tinha relatado o mesmo hoje."
                    },
                    "reason": {
                      "type": "string",
                      "description": "Por que não contou; só aparece quando `counted` é `false`."
                    },
                    "channel_id": {
                      "type": "string",
                      "description": "O canal relatado."
                    },
                    "stats": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Social"
                        }
                      ],
                      "description": "Os contadores do canal já com este relato dentro."
                    },
                    "health": {
                      "type": "string",
                      "description": "URL do painel de saúde completo deste canal."
                    }
                  },
                  "required": [
                    "ok",
                    "counted",
                    "channel_id",
                    "stats",
                    "health"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`channel_id` ausente, `ok` faltando ou `code` fora da lista."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      }
    },
    "/api/play-reports": {
      "get": {
        "operationId": "play_reports",
        "summary": "Relatos crus, com endereço IP, para investigar um canal — só operador.",
        "description": "Existe separado do painel público justamente porque traz endereço. A linha some depois do prazo em `retention_days`, e `/api/channels/:id/health` nunca devolve IP.\nDevolve: { items[{channel_id,ok,code,ip,browser,os,country,day,at}], limit, offset, next_offset, retention_days, filters{channel_id,only_failures}, api }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "channel_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restringe a um canal.",
            "example": "GloboNews.br"
          },
          {
            "name": "ok",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "enum": [
                "0"
              ]
            },
            "description": "`0` traz só as falhas — é o recorte que interessa numa investigação."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Itens por página. Acima de 50 é silenciosamente reduzido a 50."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Quantos itens pular. Use `next_offset` da resposta anterior."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{channel_id,ok,code,ip,browser,os,country,day,at}], limit, offset, next_offset, retention_days, filters{channel_id,only_failures}, api }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginaDeRelatos"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      }
    },
    "/api/favorites": {
      "get": {
        "operationId": "list_favorites",
        "summary": "Canais favoritados pelo dono, do mais recente para o mais antigo.",
        "description": "Exibe somente canais disponíveis no catálogo.\nDevolve: { items[{channel_id,name,country,quality,logo_url,playable_hint,favorites,created_at,stale,api}], total, limit, offset, next_offset, next, api, max }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Itens por página. Acima de 50 é silenciosamente reduzido a 50."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Quantos itens pular. Use `next_offset` da resposta anterior."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{channel_id,name,country,quality,logo_url,playable_hint,favorites,created_at,stale,api}], total, limit, offset, next_offset, next, api, max }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginaDeFavoritos"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      },
      "post": {
        "operationId": "add_favorite",
        "summary": "Favorita um canal. Repetir não soma: o contador público conta pessoas, não cliques.",
        "description": "Devolve: { ok, favorited, created, channel_id, favorites }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel_id": {
                    "type": "string",
                    "description": "Canal a favoritar, ex. `GloboNews.br`."
                  }
                },
                "required": [
                  "channel_id"
                ]
              },
              "example": {
                "channel_id": "GloboNews.br"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, favorited, created, channel_id, favorites }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true`."
                    },
                    "favorited": {
                      "type": "boolean",
                      "description": "Sempre `true` ao fim desta chamada."
                    },
                    "created": {
                      "type": "boolean",
                      "description": "`true` se foi agora; `false` se já era favorito."
                    },
                    "channel_id": {
                      "type": "string",
                      "description": "O canal favoritado."
                    },
                    "favorites": {
                      "type": "integer",
                      "description": "Total de pessoas que favoritaram este canal."
                    }
                  },
                  "required": [
                    "ok",
                    "favorited",
                    "created",
                    "channel_id",
                    "favorites"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`channel_id` ausente ou teto de favoritos atingido."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      }
    },
    "/api/favorites/{channel_id}": {
      "delete": {
        "operationId": "remove_favorite",
        "summary": "Desfavorita o canal e devolve o ponto ao contador público.",
        "description": "Devolve: { ok, favorited, channel_id, favorites }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "channel_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Canal a desfavoritar, ex. `GloboNews.br`.",
            "example": "GloboNews.br"
          }
        ],
        "responses": {
          "200": {
            "description": "{ ok, favorited, channel_id, favorites }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true`."
                    },
                    "favorited": {
                      "type": "boolean",
                      "description": "Sempre `false` ao fim desta chamada."
                    },
                    "channel_id": {
                      "type": "string",
                      "description": "O canal que saiu dos favoritos."
                    },
                    "favorites": {
                      "type": "integer",
                      "description": "Total de pessoas que ainda favoritam este canal."
                    }
                  },
                  "required": [
                    "ok",
                    "favorited",
                    "channel_id",
                    "favorites"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "O canal não estava nos seus favoritos."
          }
        }
      }
    },
    "/api/channels/{id}/comments": {
      "get": {
        "operationId": "list_comments",
        "summary": "Comentários públicos de um canal, do mais novo para o mais antigo.",
        "description": "Com credencial na chamada, cada comentário seu vem com `mine: true` — é assim que a interface sabe o que dá para apagar.\nDevolve: { items[{id,channel_id,author,body,created_at,mine,api}], total, limit, offset, next_offset, next, api, channel_id, max_length }",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do canal no catálogo.",
            "example": "GloboNews.br"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Itens por página. Acima de 50 é silenciosamente reduzido a 50."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Quantos itens pular. Use `next_offset` da resposta anterior."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{id,channel_id,author,body,created_at,mine,api}], total, limit, offset, next_offset, next, api, channel_id, max_length }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginaDeComentarios"
                }
              }
            }
          },
          "404": {
            "description": "Canal não existe no catálogo."
          }
        }
      },
      "post": {
        "operationId": "post_comment",
        "summary": "Escreve um comentário no canal. Teto de 20 por hora por dono.",
        "description": "Sem `author`, o apelido é gerado e fica estável para o mesmo dono — a pessoa não vira um nome diferente a cada mensagem.\nDevolve: { ok, comment{id,channel_id,author,body,created_at,mine,api} }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do canal no catálogo.",
            "example": "GloboNews.br"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "O texto do comentário; o teto vem em `max_length` da listagem."
                  },
                  "author": {
                    "type": "string",
                    "description": "Apelido a usar; sem ele o servidor gera um estável."
                  }
                },
                "required": [
                  "body"
                ]
              },
              "example": {
                "body": "Só abre no formato 720p.",
                "author": "Wendel"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, comment{id,channel_id,author,body,created_at,mine,api} }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true` quando o comentário entrou."
                    },
                    "comment": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Comentario"
                        }
                      ],
                      "description": "O comentário criado, do jeito que ele aparece na listagem."
                    }
                  },
                  "required": [
                    "ok",
                    "comment"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Texto vazio ou acima de `max_length`."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Canal não existe."
          },
          "429": {
            "description": "Passou de 20 comentários na hora."
          }
        }
      }
    },
    "/api/comments/{id}": {
      "delete": {
        "operationId": "delete_comment",
        "summary": "Apaga um comentário seu. Comentário alheio responde 404, não 403.",
        "description": "O 404 é de propósito: a API não confirma que existe um comentário com aquele id se ele não é seu.\nDevolve: { ok, removed, channel_id }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do comentário, vindo de `Comentario.id`.",
            "example": "cm_9f3c2b1d7a4e58b0c2"
          }
        ],
        "responses": {
          "200": {
            "description": "{ ok, removed, channel_id }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true`."
                    },
                    "removed": {
                      "type": "string",
                      "description": "O id que saiu."
                    },
                    "channel_id": {
                      "type": "string",
                      "description": "Canal de onde o comentário saiu."
                    }
                  },
                  "required": [
                    "ok",
                    "removed",
                    "channel_id"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "404": {
            "description": "Recurso não existe (ou não é seu — a API não distingue os dois de propósito)."
          }
        }
      }
    },
    "/api/chat/{channel_id}/mensagens": {
      "get": {
        "operationId": "chat_history",
        "summary": "Últimas mensagens da sala do canal, mais o endereço do WebSocket para acompanhar ao vivo.",
        "description": "Devolve: { channel_id, items[{id,autor,body,at}], watching, max_length, _links{self,websocket,channel} }",
        "security": [],
        "parameters": [
          {
            "name": "channel_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do canal no catálogo.",
            "example": "GloboNews.br"
          }
        ],
        "responses": {
          "200": {
            "description": "{ channel_id, items[{id,autor,body,at}], watching, max_length, _links{self,websocket,channel} }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "channel_id": {
                      "type": "string",
                      "description": "Canal a que a sala pertence."
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MensagemChat"
                      },
                      "description": "As últimas mensagens, da mais antiga para a mais nova."
                    },
                    "watching": {
                      "type": "integer",
                      "description": "Quantas pessoas estão com a sala aberta agora."
                    },
                    "max_length": {
                      "type": "integer",
                      "description": "Tamanho máximo de uma mensagem."
                    },
                    "_links": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LinksChat"
                        }
                      ],
                      "description": "Esta listagem, o WebSocket e a ficha do canal."
                    }
                  },
                  "required": [
                    "channel_id",
                    "items",
                    "watching",
                    "max_length",
                    "_links"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Canal não existe no catálogo."
          }
        }
      },
      "post": {
        "operationId": "chat_send",
        "summary": "Manda mensagem na sala sem abrir WebSocket. Exige o passe mensal do chat.",
        "description": "Ler é grátis; escrever custa $0.10 por 30 dias. Sem passe válido a resposta é 402 com `accepts[]` — pague e repita a mesma chamada. Quem fala é o convidado (`X-Guest-Token`), o mesmo que o WebSocket reconhece; sem convidado, a sessão da conta.\nDevolve: { ok, message{id,autor,body,at} }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "channel_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do canal no catálogo.",
            "example": "GloboNews.br"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "O texto da mensagem, dentro de `max_length`."
                  },
                  "author": {
                    "type": "string",
                    "description": "Apelido a usar; sem ele o servidor gera um estável."
                  }
                },
                "required": [
                  "body"
                ]
              },
              "example": {
                "body": "alguém aí?",
                "author": "Wendel"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, message{id,autor,body,at} }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true` quando a mensagem entrou."
                    },
                    "message": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/MensagemChat"
                        }
                      ],
                      "description": "A mensagem publicada na sala."
                    }
                  },
                  "required": [
                    "ok",
                    "message"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Texto vazio ou longo demais."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "402": {
            "description": "Cota estourada. A resposta traz `accepts[]` (x402, USDC na Base): pague e repita a mesma chamada com `X-PAYMENT`."
          },
          "404": {
            "description": "Canal não existe."
          }
        }
      }
    },
    "/api/chat/pass": {
      "post": {
        "operationId": "chat_pass",
        "summary": "Compra ou confirma o passe mensal do chat: $0.10 por 30 dias, via x402 ou crédito.",
        "description": "Passe já válido devolve 200 sem cobrar de novo — dá para chamar antes de escrever, sem risco de pagar duas vezes. O passe é de quem fala no chat: o convidado de `X-Guest-Token` (é ele que o WebSocket reconhece no `hello`); sem convidado, a sessão da conta, que fala só por HTTP. Fica no registro global de compras (`direito`).\nDevolve: { ok, charged, until }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "{ ok, charged, until }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true` quando o passe está valendo ao fim da chamada."
                    },
                    "charged": {
                      "type": "boolean",
                      "description": "`true` se esta chamada cobrou; `false` se o passe já valia."
                    },
                    "until": {
                      "type": "string",
                      "description": "Até quando o passe vale (UTC); `null` com o chat aberto de graça (`X402_GRATIS`).",
                      "nullable": true
                    }
                  },
                  "required": [
                    "ok",
                    "charged",
                    "until"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "402": {
            "description": "Cota estourada. A resposta traz `accepts[]` (x402, USDC na Base): pague e repita a mesma chamada com `X-PAYMENT`."
          },
          "410": {
            "description": "Chave `iptk_…` (retirada): use o convidado."
          },
          "503": {
            "description": "`pass_unavailable` (sem registro de compras, nada cobrado) ou `pass_not_recorded` (pago, não gravado: traz o recibo)."
          }
        }
      }
    },
    "/api/chat/{channel_id}/ws": {
      "get": {
        "operationId": "get_api_chat_by_channel_id_ws",
        "summary": "WebSocket da sala do canal — o caminho ao vivo, com presença.",
        "description": "Exige `Upgrade: websocket`; sem isso responde 426. O socket entra MUDO: mande `{t:'hello',token:'ipt_…'}` e depois `{t:'msg',body:'…'}`. Você recebe `{t:'pronto',autor,items[],watching}` na entrada e `{t:'msg'|'presenca',…}` durante a sessão; erro chega como `{t:'erro',code:'auth'|'vazio'|'pago'}`.\nDevolve: `101 Switching Protocols` e a conexão WebSocket; `426` sem o header de upgrade.",
        "security": [],
        "parameters": [
          {
            "name": "channel_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID do canal no catálogo.",
            "example": "GloboNews.br"
          }
        ],
        "responses": {
          "200": {
            "description": "`101 Switching Protocols` e a conexão WebSocket; `426` sem o header de upgrade."
          },
          "404": {
            "description": "Canal não existe no catálogo."
          }
        }
      }
    },
    "/api/auth/bootstrap": {
      "get": {
        "operationId": "get_api_auth_bootstrap",
        "summary": "Prepara o navegador para entrar na conta global.",
        "description": "Define cookie HttpOnly restrito ao host. CSRF vinculado à sessão atual. Sem CORS.\nDevolve: { csrf, context }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ csrf, context }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "csrf": {
                      "type": "string",
                      "description": "X-CSRF-Token"
                    },
                    "context": {
                      "type": "string",
                      "description": "Opaque view context, also in X-MM-Context; not a credential / contexto opaco da vista, não é credencial."
                    }
                  },
                  "required": [
                    "csrf",
                    "context"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_request"
          },
          "403": {
            "description": "invalid_origin / invalid_csrf"
          },
          "503": {
            "description": "auth_unavailable: a sessão anterior é preservada / the previous session is preserved"
          }
        }
      }
    },
    "/api/account/profile": {
      "get": {
        "operationId": "get_api_account_profile",
        "summary": "Consulta seu perfil global.",
        "description": "Lê preferências atuais da conta. Altere-as na página da conta; produtos não mantêm perfil autoritativo separado.\nDevolve: {profile:{name,locale,timeZone,theme,revision}}",
        "security": [
          {
            "globalAccount": []
          }
        ],
        "responses": {
          "200": {
            "description": "{profile:{name,locale,timeZone,theme,revision}}"
          },
          "401": {
            "description": "invalid_session"
          },
          "503": {
            "description": "auth_unavailable"
          }
        }
      }
    },
    "/api/account/avatar": {
      "get": {
        "operationId": "get_api_account_avatar",
        "summary": "Consulta sua foto de perfil global.",
        "description": "WebP privado de até 64 KiB, sem cache. Altere-o na conta. Não aceita ID de usuário ou URL de objeto.\nDevolve: image/webp; Cache-Control: no-store",
        "security": [
          {
            "globalAccount": []
          }
        ],
        "responses": {
          "200": {
            "description": "image/webp; Cache-Control: no-store",
            "content": {
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "invalid_session"
          },
          "404": {
            "description": "not_found: no photo / sem foto"
          },
          "503": {
            "description": "auth_unavailable"
          }
        }
      }
    },
    "/api/auth/logout": {
      "post": {
        "operationId": "post_api_auth_logout",
        "summary": "Revoga esta sessão do produto.",
        "description": "Exige bootstrap/CSRF deste navegador e sessão. As sessões de outros produtos permanecem ativas.\nDevolve: { ok }",
        "security": [
          {
            "globalAccount": []
          }
        ],
        "responses": {
          "200": {
            "description": "{ ok }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "true"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_request"
          },
          "403": {
            "description": "invalid_origin / invalid_csrf"
          },
          "503": {
            "description": "auth_unavailable: a sessão anterior é preservada / the previous session is preserved"
          }
        }
      }
    },
    "/api/account/keys": {
      "get": {
        "operationId": "get_api_account_keys",
        "summary": "Lista suas chaves de API neste produto.",
        "description": "Nunca devolve a chave: nome, 4 últimos caracteres, organização, criação, último uso (por hora) e se ainda vale.\nDevolve: { keys }",
        "security": [
          {
            "globalAccount": []
          }
        ],
        "responses": {
          "200": {
            "description": "{ keys }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "`id`, `name`, `organizationId`, `last4`, `createdAt`, `lastUsedAt`, `revokedAt`, `active` (false quando revogada ou parada por troca de senha / encerrar todos os acessos)."
                    }
                  },
                  "required": [
                    "keys"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "invalid_session"
          },
          "503": {
            "description": "auth_unavailable"
          }
        }
      }
    },
    "/api/account/keys/create": {
      "post": {
        "operationId": "post_api_account_keys_create",
        "summary": "Cria uma chave de API para agentes e scripts.",
        "description": "Exige entrada nos últimos 5 minutos; a de organização também exige segundo fator na sessão e o papel de dona/administradora com o produto ligado. No máximo 10 chaves vivas por conta e produto. A chave (`secret`) volta UMA vez.\nDevolve: { key, secret }",
        "security": [
          {
            "globalAccount": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Até 60 caracteres."
                  },
                  "organizationId": {
                    "type": "string",
                    "description": "`null` para chave da conta."
                  }
                },
                "required": [
                  "name",
                  "organizationId"
                ]
              },
              "example": {
                "name": "agent",
                "organizationId": null
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ key, secret }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "object",
                      "description": "`id`, `name`, `organizationId`, `last4`, `createdAt`."
                    },
                    "secret": {
                      "type": "string",
                      "description": "`mmk_…`, mostrada uma vez."
                    }
                  },
                  "required": [
                    "key",
                    "secret"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_key_name / invalid_organization"
          },
          "401": {
            "description": "invalid_session / reauth_required"
          },
          "403": {
            "description": "invalid_origin / invalid_csrf / organization_forbidden / organization_mfa_required"
          },
          "409": {
            "description": "key_limit_reached"
          },
          "503": {
            "description": "auth_unavailable"
          }
        }
      }
    },
    "/api/account/keys/revoke": {
      "post": {
        "operationId": "post_api_account_keys_revoke",
        "summary": "Revoga uma das suas chaves de API.",
        "description": "Para a chave na hora. Repetir não faz mal.\nDevolve: { ok }",
        "security": [
          {
            "globalAccount": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "O `id` da chave."
                  }
                },
                "required": [
                  "id"
                ]
              },
              "example": {
                "id": "…"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "true"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_key_id"
          },
          "401": {
            "description": "invalid_session"
          },
          "403": {
            "description": "invalid_origin / invalid_csrf"
          },
          "404": {
            "description": "key_not_found"
          },
          "503": {
            "description": "auth_unavailable"
          }
        }
      }
    },
    "/api/auth/claim": {
      "post": {
        "operationId": "post_api_auth_claim",
        "summary": "Passa para a conta o que o convidado criou: pastas (com as sub-abas e os canais), favoritos, histórico, comentários e o link do feed.",
        "description": "Exige bootstrap/CSRF deste navegador; a página faz isso logo depois de entrar. Só passa o que o convidado ainda tem, numa transação, e o que colide com o que a conta já tem fica com o convidado. O que o convidado comprou passa junto. Token antigo, sem assinatura, que não é dono de nada aqui é recusado. Repetir não faz mal (move zero).\nDevolve: { ok, claimed }",
        "security": [
          {
            "globalAccount": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "guest_token": {
                    "type": "string",
                    "description": "Convidado `ipt_…` deste navegador."
                  }
                },
                "required": [
                  "guest_token"
                ]
              },
              "example": {
                "guest_token": "ipt_…"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, claimed }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Se o convidado foi reconhecido e passou."
                    },
                    "claimed": {
                      "type": "object",
                      "description": "`product.movidos` (linhas que mudaram de dono, por tabela), `product.apagados` (duplicatas do convidado descartadas) e `product.direitos` (compras que passaram)."
                    }
                  },
                  "required": [
                    "ok",
                    "claimed"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_product_claim / invalid_body"
          },
          "401": {
            "description": "invalid_session"
          },
          "403": {
            "description": "invalid_origin / invalid_csrf"
          },
          "409": {
            "description": "unknown_guest (em `claimed.reason` / in `claimed.reason`)"
          },
          "503": {
            "description": "product_claim_pending / auth_unavailable"
          }
        }
      }
    },
    "/api/me": {
      "get": {
        "operationId": "me",
        "summary": "A conta da sessão: e-mail e o tamanho da biblioteca dela.",
        "description": "A conta é a da biblioteca de conta; `user.id` é o id da conta, e é ele o dono da galeria com sessão.\nDevolve: { user{id,email}, profile, app, resources{categories,favorites,history} }",
        "security": [
          {
            "globalAccount": []
          }
        ],
        "responses": {
          "200": {
            "description": "{ user{id,email}, profile, app, resources{categories,favorites,history} }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Conta"
                        }
                      ],
                      "description": "A pessoa dona da sessão."
                    },
                    "profile": {
                      "type": "object",
                      "description": "Perfil global: `name`, `locale`, `timeZone`, `theme`, `revision`."
                    },
                    "app": {
                      "type": "string",
                      "description": "Nome do produto."
                    },
                    "resources": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Recursos"
                        }
                      ],
                      "description": "Quantas pastas, favoritos e canais no histórico a conta tem."
                    }
                  },
                  "required": [
                    "user",
                    "profile",
                    "app",
                    "resources"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "invalid_session"
          },
          "503": {
            "description": "auth_unavailable"
          }
        }
      }
    },
    "/api/billing": {
      "get": {
        "operationId": "billing",
        "summary": "Preços em vigor, tetos da galeria e a configuração x402 completa.",
        "description": "É o número EM VIGOR: leia daqui antes de gastar chamada, em vez de assumir o preço da documentação. Com credencial, também diz se o passe de chat de quem fala no chat (o convidado; sem ele, a conta) está ativo. `prices.abuso_24h_usd` é o preço da porta de UA vazio/curl, hoje desligada.\nDevolve: { provider, mode, network, chain_id, pay_to, homolog, dev, dev_gate, gratis?, facilitator, asset, asset_address, faucet, wallets, product, prices{contact_agent_usd,chat_month_usd,abuso_24h_usd}, chat{active,until}, limits{categories,groups_per_category,items_per_group} }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ provider, mode, network, chain_id, pay_to, homolog, dev, dev_gate, gratis?, facilitator, asset, asset_address, faucet, wallets, product, prices{contact_agent_usd,chat_month_usd,abuso_24h_usd}, chat{active,until}, limits{categories,groups_per_category,items_per_group} }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Billing"
                }
              }
            }
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "operationId": "contact",
        "summary": "Contato e projetos de produtores: humano usa Turnstile; agente paga $0.10 por x402 ou crédito.",
        "description": "Projetos de produtores: consulte GET /api/producers e use contact.message_template na mensagem. O primeiro envio de agente sai sem espera, após pagamento; depois o backoff é 60s dobrando até 1 hora (`Retry-After`). O valor paga somente o envio do contato, não o serviço de transmissão.\nDevolve: { ok }",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Como chamar quem escreveu."
                  },
                  "email": {
                    "type": "string",
                    "description": "Para onde responder."
                  },
                  "message": {
                    "type": "string",
                    "description": "O que você quer dizer."
                  },
                  "form_ts": {
                    "type": "integer",
                    "description": "Início da composição, em milissegundos Unix: entre 2 segundos e 12 horas atrás, obrigatório também para agentes."
                  },
                  "cf_turnstile_response": {
                    "type": "string",
                    "description": "Token Turnstile do formulário humano; ausente segue pelo pagamento de agente."
                  },
                  "tipo": {
                    "type": "string",
                    "description": "Proposta: `patrocinio`, `parceria` ou `anuncio`. Liga os campos abaixo."
                  },
                  "empresa": {
                    "type": "string",
                    "description": "Quem propõe, quando é empresa."
                  },
                  "site": {
                    "type": "string",
                    "description": "Site de quem propõe."
                  },
                  "orcamento": {
                    "type": "string",
                    "description": "`ate_100`, `100_500`, `500_2000`, `2000_mais` ou `a_combinar`."
                  },
                  "espaco": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Ids de placement de `GET /api/partners`, até 6."
                  },
                  "duracao": {
                    "type": "string",
                    "description": "Dias de exposição: `30`, `90` ou `365`."
                  },
                  "pagamento": {
                    "type": "string",
                    "description": "`usdc`, `deposito` ou `a_combinar`."
                  }
                },
                "required": [
                  "name",
                  "email",
                  "message",
                  "form_ts"
                ]
              },
              "example": {
                "name": "…",
                "email": "a@example.com",
                "message": "…"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Campo obrigatório faltando."
          },
          "402": {
            "description": "Cota estourada. A resposta traz `accepts[]` (x402, USDC na Base): pague e repita a mesma chamada com `X-PAYMENT`."
          },
          "403": {
            "description": "Verificação Turnstile inválida."
          },
          "429": {
            "description": "Backoff de agente: espere o `Retry-After`."
          },
          "502": {
            "description": "O provedor não aceitou o envio do e-mail; tente novamente mais tarde."
          },
          "503": {
            "description": "Envio indisponível por configuração de e-mail incompleta."
          }
        }
      }
    },
    "/api/visit": {
      "post": {
        "operationId": "post_api_visit",
        "summary": "Ping da interface que incrementa a visita do dia. Agente não precisa chamar.",
        "description": "Smoke não conta: `X-MM-Smoke`, User-Agent `mm-smoke` ou `smoke: true` no corpo entram como `counted: false`.\nDevolve: { ok, counted, reason? }",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "smoke": {
                    "type": "boolean",
                    "description": "`true` marca a chamada como teste e ela não entra na contagem."
                  }
                }
              },
              "example": {
                "smoke": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, counted, reason? }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Sempre `true`."
                    },
                    "counted": {
                      "type": "boolean",
                      "description": "Se a visita entrou na contagem do dia."
                    },
                    "reason": {
                      "type": "string",
                      "description": "Por que não contou, quando `counted` é `false`."
                    }
                  },
                  "required": [
                    "ok",
                    "counted"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/erro-cliente": {
      "post": {
        "operationId": "post_api_erro_cliente",
        "summary": "Relato de erro do navegador, enviado pela própria interface. Agente não precisa chamar.",
        "description": "A interface relata sozinha erro de JS, promessa rejeitada, script/CSS que não carregou e bloqueio de CSP — uma vez por sessão — e o app relata falha tratada por `window.mmErro.relata`. O servidor valida o envelope, redige credencial, e-mail e telefone, junta repetições da mesma falha por minuto e registra um evento operacional; nada é gravado em banco. Não guarda IP, cookie, query nem o User-Agent inteiro. Responde 204 sempre, inclusive para relato inválido.\nDevolve: 204 sem corpo, sempre — relato inválido, repetido ou acima do teto também recebe 204.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "Código da falha, `UI-` + letras/dígitos (`UI-JS-001` erro global, `UI-PROMESSA-001`, `UI-RECURSO-001`, `UI-CSP-001`, `UI-APP-001` relato do app)."
                  },
                  "phase": {
                    "type": "string",
                    "description": "Fase em que quebrou, minúsculas: `global`, `promessa`, `script`, `carregar_lista`…"
                  },
                  "path": {
                    "type": "string",
                    "description": "Caminho da página aberta, sem query."
                  },
                  "message": {
                    "type": "string",
                    "description": "Mensagem do erro, até 2000 caracteres."
                  },
                  "stack": {
                    "type": "string",
                    "description": "Stack trace, até 12000 caracteres."
                  },
                  "source": {
                    "type": "string",
                    "description": "Script de origem; só o caminho é guardado."
                  },
                  "line": {
                    "type": "integer",
                    "description": "Linha no script de origem."
                  },
                  "column": {
                    "type": "integer",
                    "description": "Coluna no script de origem."
                  },
                  "visivel": {
                    "type": "boolean",
                    "description": "Se a aba estava visível quando quebrou."
                  }
                },
                "required": [
                  "code",
                  "phase"
                ]
              },
              "example": {
                "code": "UI-APP-001",
                "phase": "carregar_lista",
                "path": "/",
                "message": "lista 500"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "204 sem corpo, sempre — relato inválido, repetido ou acima do teto também recebe 204."
          }
        }
      }
    },
    "/api/pagamento/aberto": {
      "post": {
        "operationId": "post_api_pagamento_aberto",
        "summary": "A interface relata que exibiu uma cobrança. Agentes não devem chamar.",
        "description": "Relato sem corpo, da mesma origem, enviado automaticamente quando uma cobrança fica visível. Não inicia pagamento, não concede acesso e não recebe identidade ou credencial. Não grava banco por relato. Conta eventos, não pessoas únicas. O painel privado do operador separa pedidos de pagamento da API e aberturas da interface por dia UTC; os dois números podem se sobrepor.\nDevolve: 202 sem corpo se aceito; 204 se ignorado. Sempre no-store.",
        "security": [],
        "parameters": [
          {
            "name": "Origin",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A origem da página, idêntica à desta rota."
          },
          {
            "name": "Sec-Fetch-Site",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`same-origin`, definido pelo navegador."
          },
          {
            "name": "X-MM-Payment-View",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`1`, definido pelo componente comum."
          }
        ],
        "responses": {
          "202": {
            "description": "202 sem corpo se aceito; 204 se ignorado. Sempre no-store."
          }
        }
      }
    },
    "/api/vitrine": {
      "get": {
        "operationId": "get_api_vitrine",
        "summary": "Os números públicos do produto: tráfego, agentes, uso e confiabilidade, sem dinheiro.",
        "description": "Projeção publicada de hora em hora pelo coletor da casa, arredondada a dois dígitos significativos; `null` é medição ausente, nunca zero. Cache de 15 minutos com ETag (`If-None-Match` → 304). Não há como enviar números por esta rota: a publicação é do coletor, com token próprio.\nDevolve: { v, produto, publicado, atualizado_em, stale, nome?, desde?, fuso?, hoje?, dias?, janelas?, visitantes?, pessoas?, agentes?, superficies?, mcp?, uso?, contas?, confiabilidade?, catalogo?, apoio? }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ v, produto, publicado, atualizado_em, stale, nome?, desde?, fuso?, hoje?, dias?, janelas?, visitantes?, pessoas?, agentes?, superficies?, mcp?, uso?, contas?, confiabilidade?, catalogo?, apoio? }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "v": {
                      "type": "integer",
                      "description": "Versão do contrato (1)."
                    },
                    "produto": {
                      "type": "string",
                      "description": "Id do produto."
                    },
                    "publicado": {
                      "type": "boolean",
                      "description": "`false` antes da primeira publicação do coletor; aí só estas cinco chaves vêm."
                    },
                    "atualizado_em": {
                      "type": "string",
                      "description": "Quando o coletor publicou (ISO 8601).",
                      "nullable": true
                    },
                    "stale": {
                      "type": "boolean",
                      "description": "`true` quando a projeção tem mais de 26 h."
                    },
                    "nome": {
                      "type": "string",
                      "description": "Nome do produto."
                    },
                    "desde": {
                      "type": "string",
                      "description": "Dia a partir do qual a série vale.",
                      "nullable": true
                    },
                    "fuso": {
                      "type": "string",
                      "description": "Fuso dos dias (`UTC`)."
                    },
                    "hoje": {
                      "type": "object",
                      "description": "O dia de hoje: páginas por classe (pessoa, IA, bot), chamadas de API por classe, leituras das superfícies de máquina e uso do produto."
                    },
                    "dias": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Até 31 dias, o mais antigo primeiro: `dia`, `paginas`, `api`, `api_ia`, `maquina`, `visitantes`, `uso`."
                    },
                    "janelas": {
                      "type": "object",
                      "description": "Somas de 7 e 30 dias (`d7`, `d30`)."
                    },
                    "visitantes": {
                      "type": "object",
                      "description": "Visitantes únicos na borda em 7 dias."
                    },
                    "pessoas": {
                      "type": "object",
                      "description": "GA4 quando há: usuários, sessões, países, aparelhos e quem chegou de IA.",
                      "nullable": true
                    },
                    "agentes": {
                      "type": "object",
                      "description": "Os agentes de IA e os bots que mais leem, 7 dias."
                    },
                    "superficies": {
                      "type": "object",
                      "description": "Leituras de OKF, llms, well-known, OpenAPI e MCP em 7 dias."
                    },
                    "mcp": {
                      "type": "object",
                      "description": "Chamadas MCP em 7 dias."
                    },
                    "uso": {
                      "type": "object",
                      "description": "Uso real do produto por recurso: rótulo, hoje, 7 e 30 dias."
                    },
                    "contas": {
                      "type": "object",
                      "description": "Usuários e convidados.",
                      "nullable": true
                    },
                    "confiabilidade": {
                      "type": "object",
                      "description": "Percentual de pedidos sem 5xx em 7 dias e o build no ar."
                    },
                    "catalogo": {
                      "type": "object",
                      "description": "Tamanho do acervo, quando o produto tem um.",
                      "nullable": true
                    },
                    "apoio": {
                      "type": "object",
                      "description": "Impressões e cliques por patrocinador, quando houver."
                    }
                  },
                  "required": [
                    "v",
                    "produto",
                    "publicado",
                    "atualizado_em",
                    "stale"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/vitrine/operador": {
      "get": {
        "operationId": "get_api_vitrine_operador",
        "summary": "O documento completo do produto no painel do operador — só com o token do operador.",
        "description": "Devolve: { produto, atualizado_em, operador }",
        "security": [],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <METRICS_TOKEN>` — a classe operador."
          }
        ],
        "responses": {
          "200": {
            "description": "{ produto, atualizado_em, operador }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "produto": {
                      "type": "string",
                      "description": "Id do produto."
                    },
                    "atualizado_em": {
                      "type": "string",
                      "description": "Quando o coletor publicou.",
                      "nullable": true
                    },
                    "operador": {
                      "type": "object",
                      "description": "O documento completo do coletor, com o que a projeção pública não carrega.",
                      "nullable": true
                    }
                  },
                  "required": [
                    "produto",
                    "atualizado_em",
                    "operador"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sem token, token errado ou token de outra classe."
          },
          "503": {
            "description": "Worker sem `METRICS_TOKEN` ou sem o control plane."
          }
        }
      }
    },
    "/api/vitrine/painel": {
      "get": {
        "operationId": "get_api_vitrine_painel",
        "summary": "O painel da casa inteira, na forma que o gm lê — só com o token do operador.",
        "description": "Devolve: { apps, updated?, totals? }",
        "security": [],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <METRICS_TOKEN>` — a classe operador."
          }
        ],
        "responses": {
          "200": {
            "description": "{ apps, updated?, totals? }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apps": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Um documento do operador por produto, em ordem de id."
                    },
                    "updated": {
                      "type": "string",
                      "description": "Quando o coletor fechou a rodada."
                    },
                    "totals": {
                      "type": "object",
                      "description": "Os totais da casa."
                    }
                  },
                  "required": [
                    "apps"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sem token, token errado ou token de outra classe."
          },
          "503": {
            "description": "Worker sem `METRICS_TOKEN` ou sem o control plane."
          }
        }
      }
    },
    "/api/vitrine/cursores": {
      "get": {
        "operationId": "get_api_vitrine_cursores",
        "summary": "O cursor de erro resolvido por produto (`borda`, `cli`) — só com o token do operador.",
        "description": "Devolve: JSON: `{ [produto]: { borda?: ISO, cli?: ISO } }`; vazio é `{}`.",
        "security": [],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <METRICS_TOKEN>` — a classe operador."
          }
        ],
        "responses": {
          "200": {
            "description": "JSON: `{ [produto]: { borda?: ISO, cli?: ISO } }`; vazio é `{}`."
          },
          "401": {
            "description": "Sem token, token errado ou token de outra classe."
          },
          "503": {
            "description": "Worker sem `METRICS_TOKEN` ou sem o control plane."
          }
        }
      }
    },
    "/api/partners": {
      "get": {
        "operationId": "get_api_partners",
        "summary": "Parceria, patrocínio e anúncio: os espaços do produto com preço sugerido, os números públicos ao lado e como propor.",
        "description": "Informação sob consulta, sem ativação: espaços do catálogo da casa com preço em USD por 30 dias (90 e 365 dias com desconto), patrocinadores em vigor, recorte de `/api/vitrine`, carteira da casa (USDC na Base) e o caminho de contato — depósito, PIX ou fatura são combinados na resposta. Cache de 1 hora.\nDevolve: { status, produto, idioma, titulo, descricao, publico, modalidades, placements, house_bundle, parcerias, current_sponsors, stats, payment, contact, politica, _links }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ status, produto, idioma, titulo, descricao, publico, modalidades, placements, house_bundle, parcerias, current_sponsors, stats, payment, contact, politica, _links }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "description": "`sob_consulta`: informação e proposta, sem ativação nem cobrança."
                    },
                    "produto": {
                      "type": "string",
                      "description": "Nome do produto."
                    },
                    "idioma": {
                      "type": "string",
                      "description": "Idioma dos textos (o do produto)."
                    },
                    "titulo": {
                      "type": "string",
                      "description": "Título da oferta."
                    },
                    "descricao": {
                      "type": "string",
                      "description": "Uma frase sobre a oferta."
                    },
                    "publico": {
                      "type": "string",
                      "description": "Quem usa o produto — o público que o patrocinador alcança."
                    },
                    "modalidades": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "`{ id, nome }`: patrocinio, parceria, anuncio."
                    },
                    "placements": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Os espaços do produto: `id`, `nome`, `onde`, `formato`, `exclusivo`, `medicao`, `price_usd_30d` (sugestão; `null` é sob consulta), `exposure[{ dias, price_usd }]` para 30, 90 e 365 dias, `disponivel`."
                    },
                    "house_bundle": {
                      "type": "object",
                      "description": "O pacote da casa: rodapé e menção para agentes nos dez produtos, com desconto."
                    },
                    "parcerias": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Ideias de parceria que o produto aceita discutir."
                    },
                    "current_sponsors": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Patrocinadores em vigor: `id`, `nome`, `url`, `frase`, `espacos`, `ate`."
                    },
                    "stats": {
                      "type": "object",
                      "description": "Recorte dos números públicos (`hoje`, `janelas`, `agentes`, `confiabilidade`) e o `link` para `/api/vitrine`; `publicado: false` antes da primeira publicação."
                    },
                    "payment": {
                      "type": "object",
                      "description": "Como pagar: `rede`, `chain_id`, `ativo`, `pay_to`, `eip681` (a carteira da casa, quando declarada), `alternativas` e a `nota` — depósito, PIX ou fatura pela resposta."
                    },
                    "contact": {
                      "type": "object",
                      "description": "`email`, `form_url`, `api_url` (`POST /api/contact` onde há handler), `campos` (os obrigatórios), `campos_proposta` (os opcionais da proposta, com os valores aceitos de cada um), `price_agent_usd`, `message_template`, `instructions`."
                    },
                    "politica": {
                      "type": "object",
                      "description": "Rótulo do espaço, setores recusados, pagamento adiantado, prazos."
                    },
                    "_links": {
                      "type": "object",
                      "description": "`self`, `stats`, `page` (`null` até a página existir), `contact`, `casa` (o mesmo caminho nos dez produtos)."
                    }
                  },
                  "required": [
                    "status",
                    "produto",
                    "idioma",
                    "titulo",
                    "descricao",
                    "publico",
                    "modalidades",
                    "placements",
                    "house_bundle",
                    "parcerias",
                    "current_sponsors",
                    "stats",
                    "payment",
                    "contact",
                    "politica",
                    "_links"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/metrics": {
      "get": {
        "operationId": "get_api_metrics",
        "summary": "Métricas dos últimos 7 dias. Com o token do operador, inclui os pagamentos.",
        "description": "Sem credencial devolve visitas e uso (`accounts` vem vazio: convidado e conta são do SDK, sem contagem por produto). Com `METRICS_TOKEN` em Bearer acrescenta `payments` — e só em Base mainnet, porque número de homologação em painel financeiro engana.\nDevolve: { app, today, today_visits, today_contacts?, today_plays, today_feeds, days, usage, accounts, top_plays, financeiro?, payments? }",
        "security": [],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <METRICS_TOKEN>` para incluir o bloco financeiro."
          }
        ],
        "responses": {
          "200": {
            "description": "{ app, today, today_visits, today_contacts?, today_plays, today_feeds, days, usage, accounts, top_plays, financeiro?, payments? }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Metricas"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/catalogo/estado": {
      "get": {
        "operationId": "get_api_admin_catalogo_estado",
        "summary": "Contagens do catálogo no ar e do staging, mais o carimbo da última recarga.",
        "description": "Credencial `CATALOGO_TOKEN`. Diagnóstico do estado publicado.\nDevolve: { live{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities}, staging{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities}, meta{synced_at,applied_at,dump_sha256,staging_run} }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "{ live{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities}, staging{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities}, meta{synced_at,applied_at,dump_sha256,staging_run} }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstadoCatalogo"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "503": {
            "description": "`CATALOGO_TOKEN` não configurado no Worker: a recarga está desligada."
          }
        }
      }
    },
    "/api/admin/catalogo/slugs": {
      "get": {
        "operationId": "get_api_admin_catalogo_slugs",
        "summary": "Slug publicado de cada canal, paginado por id — a recarga herda para não trocar URL indexada.",
        "description": "Keyset por `id`: repita com `apos` = `next_after` até vir `null`.\nDevolve: { items[{id,slug}], next_after }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "apos",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Cursor: devolve só ids maiores que este (o `next_after` da página anterior).",
            "example": "GloboRJ.br"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Tamanho da página; teto de 5000.",
            "example": 5000
          }
        ],
        "responses": {
          "200": {
            "description": "{ items[{id,slug}], next_after }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SlugsPublicados"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "503": {
            "description": "Recarga desligada (sem `CATALOGO_TOKEN`)."
          }
        }
      }
    },
    "/api/admin/catalogo/inicio": {
      "post": {
        "operationId": "post_api_admin_catalogo_inicio",
        "summary": "A abertura de staging foi retirada e responde 410.",
        "description": "Nenhum corpo reativa staging, apaga dados ou recria índices. A carga aceita somente diferença com orçamento; estado perdido exige restauração explícita.\nDevolve: 410 `recarga_completa_bloqueada`, após autenticação.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "410 `recarga_completa_bloqueada`, após autenticação."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "410": {
            "description": "Sempre: protocolo retirado."
          }
        }
      }
    },
    "/api/admin/catalogo/lote": {
      "post": {
        "operationId": "post_api_admin_catalogo_lote",
        "summary": "O envio de lote para staging foi retirado e responde 410.",
        "description": "Nenhum corpo reativa staging, apaga dados ou recria índices. A carga aceita somente diferença com orçamento; estado perdido exige restauração explícita.\nDevolve: 410 `recarga_completa_bloqueada`, após autenticação.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "410 `recarga_completa_bloqueada`, após autenticação."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "410": {
            "description": "Sempre: protocolo retirado."
          }
        }
      }
    },
    "/api/admin/guia/lote": {
      "post": {
        "operationId": "post_api_admin_guia_lote",
        "summary": "Grava o dia de programação de até 500 canais em `guia_dia` (INSERT OR REPLACE).",
        "description": "Publicação restrita à credencial de operação. Um canal por linha; programas fora de ordem são ordenados, sem título ou invertidos caem fora; JSON do canal acima de 64 KB recusa o pedido com o índice.\nDevolve: { ok, day, gravados }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "day": {
                    "type": "string",
                    "description": "Dia grabado, `YYYY-MM-DD`."
                  },
                  "canais": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "`{ channel_id, site, programas: [{ inicio, fim, titulo, desc?, categoria? }] }`, instantes ISO 8601. Teto de 500 por pedido."
                  }
                },
                "required": [
                  "day",
                  "canais"
                ]
              },
              "example": {
                "day": "2026-09-04",
                "canais": [
                  {
                    "channel_id": "RecordNews.br",
                    "site": "programacao.example",
                    "programas": [
                      {
                        "inicio": "2026-09-04T09:00:00Z",
                        "fim": "2026-09-04T10:00:00Z",
                        "titulo": "Jornal da Record"
                      }
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, day, gravados }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GuiaLoteGravado"
                }
              }
            }
          },
          "400": {
            "description": "`day` torto, `canais` vazia ou canal inválido (o índice e o motivo vêm na mensagem)."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "413": {
            "description": "Mais de 500 canais num pedido."
          }
        }
      }
    },
    "/api/admin/guia/fim": {
      "post": {
        "operationId": "post_api_admin_guia_fim",
        "summary": "Registra a fonte `guia` em `catalog_meta.fontes`, ao lado das fontes do catálogo.",
        "description": "Fecha a rodada do grabber: `fetched_at`, `itens` (canais, sites, programas…), `stale`, `ausente`. É o que `GET /api/health` mostra em `sources.guia` (limite de 2 dias) e o que o smoke cobra quando a fonte está fresca.\nDevolve: { ok, guia }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "registro": {
                    "type": "object",
                    "description": "O mesmo formato de `fontes` da troca: `fetched_at`, `sha256` (pode ser nulo), `itens`, `stale`, `ausente`, `motivo`."
                  }
                },
                "required": [
                  "registro"
                ]
              },
              "example": {
                "registro": {
                  "fetched_at": "2026-09-04T03:20:00Z",
                  "sha256": null,
                  "itens": {
                    "canais": 64,
                    "sites": 2
                  },
                  "stale": false,
                  "ausente": false
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, guia }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GuiaRegistrada"
                }
              }
            }
          },
          "400": {
            "description": "Registro com campo de tipo errado."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      }
    },
    "/api/admin/logos/mortas": {
      "get": {
        "operationId": "get_api_admin_logos_mortas",
        "summary": "Canais de TV cuja origem de logo morreu (o cron já falhou ao buscá-la) e ainda não têm override.",
        "description": "Consulta restrita à operação, para identificar logos indisponíveis.\nDevolve: { items, limit }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 2000
            },
            "description": "Quantos canais devolver (teto 5000)."
          }
        ],
        "responses": {
          "200": {
            "description": "{ items, limit }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogosMortas"
                }
              }
            }
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          }
        }
      }
    },
    "/api/admin/logos/overrides": {
      "post": {
        "operationId": "post_api_admin_logos_overrides",
        "summary": "Atualiza o logo exibido na ficha do canal.",
        "description": "Só https. Sobrevive à recarga (a tabela fica fora da troca). O crédito da fonte sai em `/sobre`.\nDevolve: { ok, fonte, gravados }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "fonte": {
                    "type": "string",
                    "description": "Identificador para atribuição."
                  },
                  "itens": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "`{ channel_id, url }`; teto de 500 por pedido."
                  }
                },
                "required": [
                  "fonte",
                  "itens"
                ]
              },
              "example": {
                "fonte": "licenciante",
                "itens": [
                  {
                    "channel_id": "BandNews.br",
                    "url": "https://logos.example/canal.png"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, fonte, gravados }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OverridesGravados"
                }
              }
            }
          },
          "400": {
            "description": "`fonte` torta, lista vazia ou item sem `channel_id`/https (o índice vem na mensagem)."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "413": {
            "description": "Mais de 500 itens."
          }
        }
      }
    },
    "/api/admin/catalogo/troca": {
      "post": {
        "operationId": "post_api_admin_catalogo_troca",
        "summary": "A troca integral do catálogo foi retirada e responde 410.",
        "description": "Nenhum corpo reativa staging, apaga dados ou recria índices. A carga aceita somente diferença com orçamento; estado perdido exige restauração explícita.\nDevolve: 410 `recarga_completa_bloqueada`, após autenticação.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "410 `recarga_completa_bloqueada`, após autenticação."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "410": {
            "description": "Sempre: protocolo retirado."
          }
        }
      }
    },
    "/api/admin/catalogo/delta/inicio": {
      "post": {
        "operationId": "post_api_admin_catalogo_delta_inicio",
        "summary": "Abre a recarga por diferença: coleira contra o catálogo no ar e a marca da execução.",
        "description": "A mesma coleira da troca, antes de tocar em qualquer linha: zero canal, zero stream, zero tocável ou menos da metade do que está no ar recusa (409, `delta_recusado`). Sobra de staging recusa e preserva os dados. `recarga_id` fica em `catalog_meta.staging_run`. Início, lotes e fim compartilham teto de 100 mil linhas escritas/dia UTC, incluindo índices e controle; CHECK atômico recusa antes do efeito.\nDevolve: { ok, recarga_id, live{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities} }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "recarga_id": {
                    "type": "string",
                    "description": "Identificador desta execução (4–64 de `[A-Za-z0-9._-]`); os pedidos seguintes só valem para a recarga que abriu."
                  },
                  "esperado": {
                    "type": "object",
                    "description": "Tabela → quantas linhas o catálogo INTEIRO terá depois da diferença (`channels_fts` conta ids distintos), mais `playable` (canais de TV tocáveis). É o que o fim confere contra o ar."
                  }
                },
                "required": [
                  "recarga_id",
                  "esperado"
                ]
              },
              "example": {
                "recarga_id": "2026-09-05T18-00-00Z",
                "esperado": {
                  "channels": 93857,
                  "channels_fts": 93857,
                  "streams": 80734,
                  "playable": 9400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, recarga_id, live{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities} }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeltaAberto"
                }
              }
            }
          },
          "400": {
            "description": "`recarga_id` fora do formato ou `esperado` ausente."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "409": {
            "description": "`delta_recusado`: a resposta lista `problemas[]` e o catálogo no ar não mudou."
          },
          "429": {
            "description": "`orcamento`: saldo diário insuficiente; preservar a diferença pendente e retomar em outra rodada."
          }
        }
      }
    },
    "/api/admin/catalogo/delta": {
      "post": {
        "operationId": "post_api_admin_catalogo_delta",
        "summary": "Aplica até 50 linhas de UMA tabela: `upsert` para entradas, `alterar` para colunas modificadas e `remover` por chave.",
        "description": "`INSERT … ON CONFLICT DO UPDATE` coluna a coluna — menos a chave e o `slug`, que é do produto (URL indexada não muda). `alterar` leva somente colunas modificadas, sem chave/slug ou FTS. Valores iguais não são regravados; FTS só apaga/reinsere texto alterado. Guarde o recibo integral para acompanhar o progresso. Reserva inclui índices e D1 `rows_written` devolve apenas o excedente medido. Ordem é do cliente: `channels` antes de `streams` nas entradas, o inverso nas remoções (FK).\nDevolve: { ok, tabela, recebidas, removidas_pedidas, gravadas, removidas }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "recarga_id": {
                    "type": "string",
                    "description": "Identificador desta execução (4–64 de `[A-Za-z0-9._-]`); os pedidos seguintes só valem para a recarga que abriu."
                  },
                  "tabela": {
                    "type": "string",
                    "description": "Uma de `channels`, `channels_fts`, `streams`, `blocklist`, `facet_countries`, `facet_categories`, `facet_languages`, `facet_subdivisions`, `facet_cities`."
                  },
                  "upsert": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Linhas com as colunas da tabela; coluna faltando entra com o DEFAULT do schema. Pode ser vazia."
                  },
                  "alterar": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "`{ chave, valores: { coluna: valor } }`: somente colunas alteradas, com null, string ou número finito. Não aceita chave, slug ou FTS."
                  },
                  "remover": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Chaves (`id`, `code` ou `channel_id`, conforme a tabela) a apagar. Pode ser vazia."
                  }
                },
                "required": [
                  "recarga_id",
                  "tabela"
                ]
              },
              "example": {
                "recarga_id": "2026-09-05T18-00-00Z",
                "tabela": "facet_countries",
                "upsert": [
                  {
                    "code": "BR",
                    "name": "Brazil"
                  }
                ],
                "remover": [
                  "XX"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, tabela, recebidas, removidas_pedidas, gravadas, removidas }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeltaAplicado"
                }
              }
            }
          },
          "400": {
            "description": "Tabela desconhecida, listas ausentes ou as duas vazias, linha sem chave, chave inválida."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "409": {
            "description": "`recarga_divergente`: a execução aberta é outra; chame `/delta/inicio`."
          },
          "413": {
            "description": "Mais de 50 linhas (`upsert` + `alterar` + `remover`) ou campo textual FTS acima de 4 KiB."
          },
          "429": {
            "description": "`orcamento`: saldo diário insuficiente; preservar a diferença pendente e retomar em outra rodada."
          }
        }
      }
    },
    "/api/admin/catalogo/delta/fim": {
      "post": {
        "operationId": "post_api_admin_catalogo_delta_fim",
        "summary": "Confere o catálogo inteiro contra `esperado` e grava o carimbo (`synced_at`) — só se bateu.",
        "description": "Contagem por tabela igual a `esperado`, um id na FTS por canal, nenhum stream órfão, nenhum canal sem slug. Diferente disso é 409 (`delta_invalido`) sem carimbo: os lotes aceitos continuam confirmados e a recarga completa permanece bloqueada. O registro por fonte (`fontes`) e o `dump_sha256` ficam em `catalog_meta`, como na troca.\nDevolve: { ok, recarga_id, depois{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities}, synced_at }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "recarga_id": {
                    "type": "string",
                    "description": "Identificador desta execução (4–64 de `[A-Za-z0-9._-]`); os pedidos seguintes só valem para a recarga que abriu."
                  },
                  "esperado": {
                    "type": "object",
                    "description": "Tabela → quantas linhas o catálogo INTEIRO terá depois da diferença (`channels_fts` conta ids distintos), mais `playable` (canais de TV tocáveis). É o que o fim confere contra o ar."
                  },
                  "dump_sha256": {
                    "type": "string",
                    "description": "SHA-256 do conjunto de dados que gerou este catálogo."
                  },
                  "origem": {
                    "type": "string",
                    "description": "Quem recarregou (ex.: `operacao`), para o relatório guardado."
                  },
                  "resumo": {
                    "type": "object",
                    "description": "Contagens da diferença (`upsert`, `remover`, `refresh`, `iguais`), só para o relatório."
                  }
                },
                "required": [
                  "recarga_id",
                  "esperado"
                ]
              },
              "example": {
                "recarga_id": "2026-09-05T18-00-00Z",
                "esperado": {
                  "channels": 93857,
                  "channels_fts": 93857,
                  "streams": 80734,
                  "playable": 9400
                },
                "dump_sha256": "…",
                "origem": "operacao",
                "resumo": {
                  "upsert": 41200,
                  "remover": 310,
                  "refresh": 7000,
                  "iguais": 190000
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, recarga_id, depois{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities}, synced_at }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelatorioDelta"
                }
              }
            }
          },
          "400": {
            "description": "`recarga_id`, `esperado` ou `fontes` inválidos."
          },
          "401": {
            "description": "Sem credencial ou credencial inválida. Veja a auth deste endpoint."
          },
          "409": {
            "description": "`recarga_divergente` (outra execução) ou `delta_invalido` — a resposta lista `problemas[]` e o carimbo não foi gravado."
          },
          "429": {
            "description": "`orcamento`: saldo diário insuficiente; preservar a diferença pendente e retomar em outra rodada."
          }
        }
      }
    },
    "/api/credito": {
      "post": {
        "operationId": "post_api_credito",
        "summary": "Recarrega crédito pré-pago: paga uma vez com x402 e recebe o token que desconta em qualquer API da casa.",
        "description": "Devolve: { token, saldo_usd, guarde, usar, saldo_em }",
        "security": [],
        "parameters": [
          {
            "name": "usd",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Pacote: 1, 5, 10 ou 25 dólares."
          }
        ],
        "responses": {
          "200": {
            "description": "{ token, saldo_usd, guarde, usar, saldo_em }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "description": "Token portador do saldo (`cred_…`). Mostrado UMA vez — não há como recuperá-lo."
                    },
                    "saldo_usd": {
                      "type": "string",
                      "description": "Saldo creditado."
                    },
                    "guarde": {
                      "type": "string",
                      "description": "Aviso de que o token é o portador do crédito."
                    },
                    "usar": {
                      "type": "string",
                      "description": "Como apresentar o token nas rotas pagas."
                    },
                    "saldo_em": {
                      "type": "string",
                      "description": "Onde consultar saldo e extrato."
                    }
                  },
                  "required": [
                    "token",
                    "saldo_usd",
                    "guarde",
                    "usar",
                    "saldo_em"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Pacote fora da lista (1, 5, 10 ou 25)."
          },
          "402": {
            "description": "Sem pagamento — o corpo traz `accepts[]` do x402."
          }
        }
      },
      "get": {
        "operationId": "get_api_credito",
        "summary": "Saldo e extrato do crédito — as últimas movimentações, sem devolver o token.",
        "description": "Devolve: { saldo_micros, saldo_usd, criado_em, movimentos }",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "{ saldo_micros, saldo_usd, criado_em, movimentos }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saldo_micros": {
                      "type": "integer",
                      "description": "Saldo em micro-dólares (1e-6 USD)."
                    },
                    "saldo_usd": {
                      "type": "string",
                      "description": "Saldo formatado."
                    },
                    "criado_em": {
                      "type": "string",
                      "description": "Quando o crédito foi aberto."
                    },
                    "movimentos": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Entradas e saídas recentes, com produto e recurso."
                    }
                  },
                  "required": [
                    "saldo_micros",
                    "saldo_usd",
                    "criado_em",
                    "movimentos"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Sem token ou token desconhecido."
          }
        }
      }
    },
    "/api/pricing": {
      "get": {
        "operationId": "pricing",
        "summary": "Preços vigentes e franquias gratuitas.",
        "description": "Devolve: { product, quota{free,paid,how_to_pay,live,free_now?,trial?}, pricing, billing, api_index }",
        "security": [],
        "responses": {
          "200": {
            "description": "{ product, quota{free,paid,how_to_pay,live,free_now?,trial?}, pricing, billing, api_index }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "product": {
                      "type": "string",
                      "description": "Product name."
                    },
                    "quota": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/PaymentQuota"
                        }
                      ],
                      "description": "Public allowances and current list prices; not personal usage."
                    },
                    "pricing": {
                      "type": "string",
                      "description": "Absolute URL of the current price list."
                    },
                    "billing": {
                      "type": "string",
                      "description": "Absolute URL of payment discovery or the existing billing summary."
                    },
                    "api_index": {
                      "type": "string",
                      "description": "Absolute URL of the API catalog."
                    }
                  },
                  "required": [
                    "product",
                    "quota",
                    "pricing",
                    "billing",
                    "api_index"
                  ]
                }
              }
            }
          },
          "405": {
            "description": "Use GET ou HEAD."
          }
        }
      }
    }
  }
}