{
  "name": "GradeTV",
  "description": "Encontre canais de TV e rádio. Consulte a programação por canal e dia. Guarde favoritos e organize listas pessoais em M3U, JSON ou XSPF. Canais ao vivo em HLS têm 120 minutos grátis por rede a cada dia UTC. O passe para dois aparelhos custa 0,10, 0,29 ou 0,99 USDC na Base por 30, 90 ou 365 dias, sem renovação automática. O pagamento fica em /pay. A emissora fornece o vídeo e o áudio. Apresente projetos de transmissão autorizada em /api/producers para avaliação, sem ativação.",
  "locales": {
    "default": "pt",
    "items": [
      {
        "code": "pt",
        "html": "pt-BR",
        "path": "/",
        "name": "Português"
      },
      {
        "code": "en",
        "html": "en",
        "path": "/en",
        "name": "English"
      },
      {
        "code": "es",
        "html": "es",
        "path": "/es",
        "name": "Español"
      },
      {
        "code": "fr",
        "html": "fr",
        "path": "/fr",
        "name": "Français"
      },
      {
        "code": "de",
        "html": "de",
        "path": "/de",
        "name": "Deutsch"
      },
      {
        "code": "hu",
        "html": "hu",
        "path": "/hu",
        "name": "Magyar"
      }
    ],
    "pages": {
      "programacao": {
        "pt": "/programacao",
        "en": "/en/programacao",
        "es": "/es/programacao",
        "fr": "/fr/programacao",
        "de": "/de/programacao",
        "hu": "/hu/programacao"
      },
      "home": {
        "pt": "/",
        "en": "/en",
        "es": "/es",
        "fr": "/fr",
        "de": "/de"
      },
      "brasil": {
        "pt": "/brasil",
        "en": "/en/brazil",
        "es": "/es/brasil",
        "fr": "/fr/bresil",
        "de": "/de/brasilien"
      },
      "guia": {
        "pt": "/como-usar",
        "en": "/en/how-to",
        "es": "/es/como-usar",
        "fr": "/fr/mode-emploi",
        "de": "/de/anleitung"
      },
      "sobre": {
        "pt": "/sobre",
        "en": "/en/about",
        "es": "/es/acerca",
        "fr": "/fr/a-propos",
        "de": "/de/ueber"
      }
    }
  },
  "auth": {
    "credito": "Token de crédito em `Authorization: Bearer cred_…` (ou `X-Credito`). Para saldo recuperado de x402, use a credencial de carteira `mmw_…` em Bearer ou `X-Wallet-Token`, emitida por `/api/auth/wallet/agent/start` e `/api/auth/wallet/agent/token`.",
    "none": "Público, sem credencial.",
    "tv_hop": "Capacidade privada HMAC X-MM-TV-Hop, ligada ao pedido e válida por 15 segundos.",
    "device": "Signed HttpOnly playback cookie. Account writes also require verified account credentials and the SDK's same-origin CSRF protection.",
    "guest": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`.",
    "session": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas.",
    "token": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
  },
  "docs": {
    "llms": "https://gradetv.net/llms.txt",
    "llms_full": "https://gradetv.net/llms-full.txt",
    "openapi": "https://gradetv.net/openapi.json",
    "mcp": "https://gradetv.net/mcp",
    "pricing": "https://gradetv.net/api/pricing",
    "billing": "https://gradetv.net/api/billing",
    "human_ui": "https://gradetv.net/",
    "data_indexes": [
      {
        "id": "tv",
        "produto": "https://gradetv.net",
        "caminho": "/programacao",
        "title": "Programação de TV e histórico",
        "description": "Consulte a grade por canal e data e abra o canal ao vivo. Apenas datas coletadas. Cada página HTML abre o app do produto com os dados públicos já renderizados.",
        "hierarchy": "Canal → data → programas",
        "url": "https://gradetv.net/programacao/",
        "formats": {
          "html": "https://gradetv.net/programacao",
          "json": "https://gradetv.net/api/programacao",
          "md": "https://gradetv.net/okf/programacao/index.md",
          "okf": "https://gradetv.net/okf/programacao/index.md"
        },
        "llms": "https://gradetv.net/llms.txt",
        "openapi": "https://gradetv.net/openapi.json",
        "mcp": "https://gradetv.net/mcp",
        "okf": "https://gradetv.net/okf/programacao/index.md",
        "access": "public-read-only",
        "pagination": {
          "max_items": 48,
          "next": "proximo"
        },
        "updates": "daily"
      },
      {
        "id": "cep",
        "produto": "https://pontofato.com",
        "caminho": "/enderecos",
        "title": "CEPs e endereços",
        "description": "Encontre endereços por lugar, com coordenadas e referência de 2022. Não certifica CEP vigente. Cada página HTML abre o app do produto com os dados públicos já renderizados.",
        "hierarchy": "UF → município → bairro/localidade → rua → endereços",
        "url": "https://pontofato.com/enderecos/",
        "formats": {
          "html": "https://pontofato.com/enderecos/",
          "json": "https://api.pontofato.com/enderecos/index.json",
          "md": "https://api.pontofato.com/enderecos/index.md",
          "okf": "https://api.pontofato.com/enderecos/index.okf.md"
        },
        "llms": "https://api.pontofato.com/enderecos/llms.txt",
        "openapi": "https://api.pontofato.com/enderecos/openapi.json",
        "mcp": "https://api.pontofato.com/enderecos/mcp",
        "okf": "https://api.pontofato.com/enderecos/okf/index.md",
        "access": "public-read-only",
        "pagination": {
          "max_items": 20,
          "next": "links.proximo"
        },
        "updates": "manual"
      },
      {
        "id": "editais",
        "produto": "https://editalmd.com",
        "caminho": "/licitacoes",
        "title": "Editais e compras públicas",
        "description": "Encontre compras públicas por lugar e período. Consulte documentos e opções de leitura no EditalMD. Cada página HTML abre o app do produto com os dados públicos já renderizados.",
        "hierarchy": "Modalidade → UF → ano → mês → dia → município → compras",
        "url": "https://editalmd.com/licitacoes/",
        "formats": {
          "html": "https://editalmd.com/licitacoes/",
          "json": "https://api.editalmd.com/licitacoes/index.json",
          "md": "https://api.editalmd.com/licitacoes/index.md",
          "okf": "https://api.editalmd.com/licitacoes/index.okf.md"
        },
        "llms": "https://api.editalmd.com/licitacoes/llms.txt",
        "openapi": "https://api.editalmd.com/licitacoes/openapi.json",
        "mcp": "https://api.editalmd.com/licitacoes/mcp",
        "okf": "https://api.editalmd.com/licitacoes/okf/index.md",
        "access": "public-read-only",
        "pagination": {
          "max_items": 20,
          "next": "links.proximo"
        },
        "updates": "manual"
      }
    ]
  },
  "producers": "https://gradetv.net/api/producers",
  "endpoints": [
    {
      "method": "GET",
      "path": "/okf/:arquivo",
      "auth": "none",
      "summary": "Bundle OKF (Open Knowledge Format v0.1): markdown com frontmatter para o agente ler o produto inteiro sem parsear HTML.",
      "grupo": "Descoberta",
      "params": {
        "arquivo": {
          "desc": "`index.md`, `sobre.md`, `api.md` ou `faq.md`.",
          "exemplo": "index.md"
        }
      },
      "retorno": {
        "_texto": "`text/markdown`. Comece por `/okf/index.md`, que lista o bundle."
      },
      "erros": {
        "404": "Arquivo fora do bundle."
      },
      "exemplo": "curl -s $ORIGIN/okf/index.md",
      "returns": "`text/markdown`. Comece por `/okf/index.md`, que lista o bundle.",
      "url": "https://gradetv.net/okf/:arquivo",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/.well-known/:arquivo",
      "auth": "none",
      "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).",
      "grupo": "Descoberta",
      "params": {
        "arquivo": {
          "desc": "`api-catalog`, `security.txt`, `x402`, `agent-card.json`, `mcp-registry-auth` ou `apis.json`.",
          "exemplo": "api-catalog"
        }
      },
      "retorno": {
        "_texto": "`application/linkset+json` no api-catalog; `application/json` no x402, no agent-card.json e no apis.json; `text/plain` nos outros dois."
      },
      "erros": {
        "404": "Nome fora dos seis publicados."
      },
      "exemplo": "curl -s $ORIGIN/.well-known/api-catalog",
      "returns": "`application/linkset+json` no api-catalog; `application/json` no x402, no agent-card.json e no apis.json; `text/plain` nos outros dois.",
      "url": "https://gradetv.net/.well-known/:arquivo",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/apis.json",
      "auth": "none",
      "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`.",
      "grupo": "Descoberta",
      "retorno": {
        "_texto": "`application/json` no formato APIs.json 0.19: `apis[]` com `baseURL`, `humanURL` e `properties[]`."
      },
      "exemplo": "curl -s $ORIGIN/apis.json",
      "returns": "`application/json` no formato APIs.json 0.19: `apis[]` com `baseURL`, `humanURL` e `properties[]`.",
      "url": "https://gradetv.net/apis.json",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/agent.json",
      "auth": "none",
      "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`.",
      "grupo": "Descoberta",
      "retorno": {
        "_texto": "`application/json`: `name`, `provider`, `protocol` (`mcp`), `interfaces[]` e `skills[]`."
      },
      "exemplo": "curl -s $ORIGIN/agent.json",
      "returns": "`application/json`: `name`, `provider`, `protocol` (`mcp`), `interfaces[]` e `skills[]`.",
      "url": "https://gradetv.net/agent.json",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/",
      "auth": "none",
      "summary": "Índice auto-descrito da API inteira, com os idiomas e as páginas HTML de cada um.",
      "grupo": "Descoberta",
      "retorno": {
        "name": {
          "tipo": "string",
          "desc": "Nome do produto."
        },
        "description": {
          "tipo": "string",
          "desc": "O que o produto faz, em uma frase."
        },
        "locales": {
          "tipo": "object",
          "desc": "Idiomas atendidos e o caminho de cada página em cada um."
        },
        "auth": {
          "tipo": "object",
          "desc": "Cada modo de autenticação e como obtê-lo."
        },
        "docs": {
          "tipo": "object",
          "desc": "Links para llms.txt, llms-full.txt, openapi.json, MCP e a UI."
        },
        "endpoints": {
          "tipo": "object[]",
          "desc": "Todo endpoint com método, caminho, auth, URL absoluta e o que devolve."
        },
        "quota": {
          "tipo": "object",
          "desc": "O que é grátis, o que custa e como pagar — antes de você gastar chamada."
        },
        "mcp": {
          "tipo": "object",
          "desc": "Endereço e transporte do servidor MCP."
        },
        "quickstart": {
          "tipo": "string[]",
          "desc": "As quatro chamadas que levam do zero à biblioteca."
        }
      },
      "returns": "{ name, description, locales, auth, docs, endpoints, quota, mcp, quickstart }",
      "url": "https://gradetv.net/api/",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/health",
      "auth": "none",
      "summary": "Liveness e o commit publicado agora — é como o smoke espera o próprio deploy.",
      "grupo": "Descoberta",
      "retorno": "Saude",
      "returns": "{ ok, app, build, ts, catalog{synced_at,age_hours,stale,limit_days}, sources }",
      "url": "https://gradetv.net/api/health",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/mcp",
      "auth": "none",
      "summary": "Servidor MCP por HTTP (Streamable HTTP, JSON-RPC 2.0) — pluga no cliente sem instalar nada.",
      "grupo": "Descoberta",
      "desc": "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.",
      "retorno": {
        "_texto": "Resposta JSON-RPC 2.0 (`initialize`, `tools/list` ou `tools/call`)."
      },
      "notes": [
        "Credencial vai nos headers de sempre (X-Guest-Token, Authorization, X-PAYMENT) e é repassada à API.",
        "Cota estourada chega como 402 com accepts[] dentro do resultado da tool — pague e repita."
      ],
      "exemplo": "curl -s -XPOST $ORIGIN/mcp -H 'content-type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'",
      "returns": "Resposta JSON-RPC 2.0 (`initialize`, `tools/list` ou `tools/call`).",
      "url": "https://gradetv.net/mcp",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/channels",
      "auth": "none",
      "colecao": {
        "porPagina": 50,
        "anda": "next_offset"
      },
      "summary": "Busca paginada do catálogo público, com as facetas de categoria da busca atual.",
      "grupo": "Catálogo",
      "desc": "É 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`.",
      "query": {
        "q": {
          "tipo": "string",
          "desc": "Texto livre no nome e nos apelidos do canal (busca full-text).",
          "exemplo": "axn"
        },
        "country": {
          "tipo": "string",
          "desc": "País do canal, ISO 3166-1 alpha-2.",
          "exemplo": "BR"
        },
        "category": {
          "tipo": "string",
          "desc": "ID de categoria do catálogo.",
          "exemplo": "news"
        },
        "language": {
          "tipo": "string",
          "desc": "Idioma do canal, ISO 639-3.",
          "exemplo": "por"
        },
        "network": {
          "tipo": "string",
          "desc": "Nome exato da rede/emissora."
        },
        "quality": {
          "tipo": "string",
          "desc": "Qualidade exata do stream.",
          "exemplo": "1080p"
        },
        "guide": {
          "tipo": "bool",
          "desc": "`1` traz só canal com grade de programação (EPG).",
          "valores": [
            "0",
            "1"
          ],
          "padrao": "0"
        },
        "subdivision": {
          "tipo": "string",
          "desc": "Estado/província, código do catálogo.",
          "exemplo": "BR-SP"
        },
        "city": {
          "tipo": "string",
          "desc": "Cidade, código do catálogo."
        },
        "playable": {
          "tipo": "bool",
          "desc": "`0` inclui canal sem stream utilizável conhecido.",
          "valores": [
            "0",
            "1"
          ],
          "padrao": "1"
        },
        "sort": {
          "tipo": "string",
          "desc": "`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.",
          "valores": [
            "name",
            "score",
            "votes"
          ],
          "padrao": "name"
        },
        "online": {
          "tipo": "bool",
          "desc": "`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.",
          "valores": [
            "0",
            "1"
          ],
          "padrao": "0"
        },
        "kind": {
          "tipo": "string",
          "desc": "`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.",
          "valores": [
            "tv",
            "radio",
            "all"
          ],
          "padrao": "tv"
        },
        "tag": {
          "tipo": "string",
          "desc": "Tag da estação de rádio (vocabulário livre, ex. `mpb`, `news`); veja `GET /api/tags`.",
          "exemplo": "mpb"
        },
        "limit": {
          "tipo": "int",
          "desc": "Itens por página. Acima de 50 é silenciosamente reduzido a 50.",
          "padrao": 20
        },
        "offset": {
          "tipo": "int",
          "desc": "Quantos itens pular. Use `next_offset` da resposta anterior.",
          "padrao": 0
        }
      },
      "retorno": "PaginaDeCanais",
      "exemplo": "curl -s '$ORIGIN/api/channels?country=BR&language=por&playable=1&limit=5'",
      "returns": "{ 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} }",
      "url": "https://gradetv.net/api/channels",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/channels/:id",
      "auth": "none",
      "summary": "Ficha completa de um canal, com os streams já apontando para o nosso hop.",
      "grupo": "Catálogo",
      "params": {
        "id": {
          "desc": "ID do canal no catálogo, ex. `GloboNews.br`.",
          "exemplo": "GloboNews.br"
        }
      },
      "retorno": "CanalCompleto",
      "erros": {
        "404": "Canal não disponível no catálogo."
      },
      "exemplo": "curl -s $ORIGIN/api/channels/GloboNews.br",
      "returns": "{ 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} }",
      "url": "https://gradetv.net/api/channels/:id",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/channels/:id/health",
      "auth": "none",
      "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.",
      "grupo": "Catálogo",
      "desc": "É 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.",
      "params": {
        "id": {
          "desc": "ID do canal no catálogo.",
          "exemplo": "GloboNews.br"
        }
      },
      "retorno": "SaudeCanal",
      "erros": {
        "404": "Canal não existe no catálogo."
      },
      "exemplo": "curl -s $ORIGIN/api/channels/GloboNews.br/health",
      "returns": "{ 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{nivel}, codes, _links{self,channel,report} }",
      "url": "https://gradetv.net/api/channels/:id/health",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/channels/:id/guia",
      "auth": "none",
      "summary": "Programação de hoje do canal, grabada por nós: o que está no ar agora e o que vem a seguir.",
      "grupo": "Catálogo",
      "desc": "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`.",
      "params": {
        "id": {
          "desc": "ID do canal no catálogo.",
          "exemplo": "RecordNews.br"
        }
      },
      "retorno": "GuiaDoDia",
      "erros": {
        "404": "Canal sem guia do dia (ou com guia velha)."
      },
      "exemplo": "curl -s $ORIGIN/api/channels/RecordNews.br/guia",
      "returns": "{ channel_id, day, site, agora{inicio,fim,titulo,categoria?,imagem?,serie?}, a_seguir{inicio,fim,titulo,categoria?,imagem?,serie?}, programas[{inicio,fim,titulo,categoria?,imagem?,serie?}] }",
      "url": "https://gradetv.net/api/channels/:id/guia",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/geo",
      "auth": "none",
      "summary": "País e idioma sugeridos para quem está chamando.",
      "desc": "Sem país detectado pela borda, sugere BR com `source: \"fallback\"`; `detected` diz o que a borda viu.",
      "grupo": "Catálogo",
      "retorno": "Geo",
      "exemplo": "curl -s $ORIGIN/api/geo",
      "returns": "{ country, detected, language, language_detected, source, api }",
      "url": "https://gradetv.net/api/geo",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/countries",
      "auth": "none",
      "summary": "Países que têm canal tocável, com a contagem e a bandeira de cada um.",
      "grupo": "Facetas",
      "query": {
        "kind": {
          "tipo": "string",
          "desc": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois.",
          "valores": [
            "tv",
            "radio",
            "all"
          ],
          "padrao": "tv"
        }
      },
      "retorno": "Lista<Pais>",
      "exemplo": "curl -s '$ORIGIN/api/countries?kind=radio'",
      "returns": "{ items[{code,name,count,flag}] }",
      "url": "https://gradetv.net/api/countries",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/tags",
      "auth": "none",
      "summary": "Tags das estações de rádio tocáveis (vocabulário livre), com a contagem de cada uma.",
      "grupo": "Facetas",
      "query": {
        "country": {
          "tipo": "string",
          "desc": "Restringe às estações de um país, ISO 3166-1 alpha-2.",
          "exemplo": "BR"
        },
        "limit": {
          "tipo": "int",
          "desc": "Quantas tags devolver (teto 100).",
          "padrao": 40
        }
      },
      "retorno": "Lista<Tag>",
      "exemplo": "curl -s '$ORIGIN/api/tags?country=BR&limit=20'",
      "returns": "{ items[{id,name,count}] }",
      "url": "https://gradetv.net/api/tags",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/categories",
      "auth": "none",
      "summary": "Vocabulário de categorias do catálogo, com ícone para a interface.",
      "grupo": "Facetas",
      "desc": "Note que `POST /api/categories` é outra coisa: cria pasta na biblioteca do dono.",
      "retorno": "Lista<Categoria>",
      "returns": "{ items[{id,name,description,icon}] }",
      "url": "https://gradetv.net/api/categories",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/languages",
      "auth": "none",
      "summary": "Idiomas que têm canal tocável, com a contagem de cada um.",
      "grupo": "Facetas",
      "query": {
        "kind": {
          "tipo": "string",
          "desc": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois.",
          "valores": [
            "tv",
            "radio",
            "all"
          ],
          "padrao": "tv"
        }
      },
      "retorno": "Lista<Idioma>",
      "exemplo": "curl -s '$ORIGIN/api/languages?kind=tv'",
      "returns": "{ items[{code,name,count}] }",
      "url": "https://gradetv.net/api/languages",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/networks",
      "auth": "none",
      "summary": "Redes e emissoras que têm canal tocável, com a contagem.",
      "grupo": "Facetas",
      "query": {
        "kind": {
          "tipo": "string",
          "desc": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois.",
          "valores": [
            "tv",
            "radio",
            "all"
          ],
          "padrao": "tv"
        }
      },
      "retorno": "Lista<Rede>",
      "exemplo": "curl -s '$ORIGIN/api/networks?kind=tv'",
      "returns": "{ items[{name,count}] }",
      "url": "https://gradetv.net/api/networks",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/qualities",
      "auth": "none",
      "summary": "Qualidades distintas encontradas nos streams do catálogo (em rádio, codec e bitrate).",
      "grupo": "Facetas",
      "query": {
        "kind": {
          "tipo": "string",
          "desc": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois.",
          "valores": [
            "tv",
            "radio",
            "all"
          ],
          "padrao": "tv"
        }
      },
      "retorno": "Lista<Qualidade>",
      "exemplo": "curl -s '$ORIGIN/api/qualities?kind=radio'",
      "returns": "{ items[{id,name,count}] }",
      "url": "https://gradetv.net/api/qualities",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/subdivisions",
      "auth": "none",
      "summary": "Estados e províncias que têm canal tocável.",
      "grupo": "Facetas",
      "query": {
        "country": {
          "tipo": "string",
          "desc": "Restringe a um país, ISO 3166-1 alpha-2.",
          "exemplo": "BR"
        },
        "kind": {
          "tipo": "string",
          "desc": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois.",
          "valores": [
            "tv",
            "radio",
            "all"
          ],
          "padrao": "tv"
        }
      },
      "retorno": "Lista<Subdivisao>",
      "exemplo": "curl -s '$ORIGIN/api/subdivisions?country=BR'",
      "returns": "{ items[{code,country,name,count}] }",
      "url": "https://gradetv.net/api/subdivisions",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/cities",
      "auth": "none",
      "summary": "Cidades que têm canal tocável, filtráveis por país e por estado.",
      "grupo": "Facetas",
      "query": {
        "country": {
          "tipo": "string",
          "desc": "Restringe a um país, ISO 3166-1 alpha-2.",
          "exemplo": "BR"
        },
        "subdivision": {
          "tipo": "string",
          "desc": "Restringe a um estado/província.",
          "exemplo": "BR-SP"
        },
        "kind": {
          "tipo": "string",
          "desc": "`tv` (padrão) conta canais de TV; `radio` conta estações de rádio; `all` junta os dois.",
          "valores": [
            "tv",
            "radio",
            "all"
          ],
          "padrao": "tv"
        }
      },
      "retorno": "Lista<Cidade>",
      "exemplo": "curl -s '$ORIGIN/api/cities?country=BR&subdivision=BR-SP'",
      "returns": "{ items[{code,country,subdivision,name,count}] }",
      "url": "https://gradetv.net/api/cities",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/programacao",
      "auth": "none",
      "grupo": "Programação",
      "summary": "Canais com programação coletada, até 48 por página.",
      "query": {
        "q": {
          "tipo": "string",
          "desc": "Busca no nome do canal em todo o acervo e, na primeira página, no nome dos programas da grade atual; até 80 caracteres."
        },
        "pais": {
          "tipo": "string",
          "desc": "Código de duas letras do país do canal (br, us); use paises."
        },
        "depois": {
          "tipo": "string",
          "desc": "ID exclusivo da última linha; use proximo."
        }
      },
      "retorno": "ColecaoProgramacao",
      "erros": {
        "400": "Cursor ou país inválido.",
        "503": "Base indisponível."
      },
      "exemplo": "curl -s $ORIGIN/api/programacao",
      "returns": "{ canais[{id,name,slug,country,logo_url,categories,languages?,network?,website?,day?,url?,timezones?,playable_hint?}], proximo, paises[{pais,canais}], programas?[{titulo,categoria,imagem?,exibicoes,mais}] }",
      "url": "https://gradetv.net/api/programacao",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/programacao/:slug",
      "auth": "none",
      "grupo": "Programação",
      "summary": "A programação de hoje do canal, no fuso dele, até 20 datas do histórico e os próximos dias coletados.",
      "params": {
        "slug": {
          "desc": "Slug persistido do canal, vindo da coleção.",
          "exemplo": "axn"
        }
      },
      "query": {
        "antes": {
          "tipo": "string",
          "desc": "Dia exclusivo para paginar os dias anteriores; use guia.proximo."
        },
        "programa": {
          "tipo": "string",
          "desc": "Nome de um programa: traz em proximos as exibições dele nos próximos dias; até 100 caracteres."
        }
      },
      "retorno": "ProgramacaoCanal",
      "erros": {
        "400": "Data inválida.",
        "404": "Canal ou programação ausente.",
        "503": "Histórico indisponível."
      },
      "exemplo": "curl -s $ORIGIN/api/programacao/axn",
      "returns": "{ canal{id,name,slug,country,logo_url,categories,languages?,network?,website?,day?,url?,timezones?,playable_hint?}, guia{canal,dia,programas,dias,proximo,seguintes}, series, proximos?[{dia,programas}], url }",
      "url": "https://gradetv.net/api/programacao/:slug",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/programacao/:slug/:dia",
      "auth": "none",
      "grupo": "Programação",
      "summary": "Programação preservada de um canal em um dia específico.",
      "params": {
        "slug": {
          "desc": "Slug persistido do canal, vindo da coleção.",
          "exemplo": "axn"
        },
        "dia": {
          "desc": "Dia da coleta, YYYY-MM-DD.",
          "exemplo": "2026-10-05"
        }
      },
      "query": {
        "antes": {
          "tipo": "string",
          "desc": "Dia exclusivo para paginar os dias anteriores; use guia.proximo."
        }
      },
      "retorno": "ProgramacaoCanal",
      "erros": {
        "400": "Data inválida.",
        "404": "Canal ou dia ausente.",
        "503": "Histórico indisponível."
      },
      "exemplo": "curl -s $ORIGIN/api/programacao/axn/2026-10-05",
      "agent_value": {
        "principal": true,
        "idioma": "pt",
        "en": {
          "when": "What is on this channel on a given day?",
          "result": "Read the collected channel schedule by date, without running your own programme collector.",
          "replaces": "We collect and organize the available schedules. Read the channel and date you need instead of building an EPG collection process.",
          "free": "Start with GET /api/programacao/:slug/:dia. Daily schedule reads are free.",
          "paid": "If you need several days in one response, compare the paid multi-day operation. It returns up to seven collected days.",
          "limits": "Check available dates. The schedule is metadata; it does not provide a video archive or broadcast rights.",
          "alternative": "You already collect the same schedule or the required date is unavailable.",
          "start": {
            "href": "/programacao",
            "label": "Find a programme"
          }
        },
        "pt": {
          "when": "O que passa neste canal em uma data?",
          "result": "Leia a programação coletada por canal e data sem operar seu próprio coletor.",
          "replaces": "Coletamos e organizamos as grades disponíveis. Consulte o canal e o dia necessários sem montar uma coleta de programação.",
          "free": "Comece por GET /api/programacao/:slug/:dia. A leitura diária é gratuita.",
          "paid": "Se precisa de vários dias em uma resposta, compare a operação paga. Ela retorna até sete dias coletados.",
          "limits": "Confira as datas disponíveis. A grade é metadado; não entrega arquivo de vídeo nem direito de transmissão.",
          "alternative": "Você já coleta a mesma grade ou a data necessária está indisponível.",
          "start": {
            "href": "/programacao",
            "label": "Consultar programação"
          }
        }
      },
      "returns": "{ canal{id,name,slug,country,logo_url,categories,languages?,network?,website?,day?,url?,timezones?,playable_hint?}, guia{canal,dia,programas,dias,proximo,seguintes}, series, proximos?[{dia,programas}], url }",
      "url": "https://gradetv.net/api/programacao/:slug/:dia",
      "auth_detail": "Público, sem credencial."
    },
    {
      "auth": "credito",
      "method": "GET",
      "path": "/api/x402/programacao/:slug",
      "grupo": "Programação",
      "summary": "Os últimos sete dias de programação do canal numa chamada, para o agente, pagos por chamada, sem conta.",
      "desc": "Até sete dias coletados do canal numa resposta, sem conta nem passe. programa seleciona por título/categoria sem caixa ou acento; agenda=1 inclui agenda_ics pronta para importar, com horários UTC e eventos únicos. O resumo informa dias consultados e exibições. A agenda é um arquivo, não uma assinatura de atualizações. A leitura diária gratuita conserva os mesmos dados. Canal, dia ou programa ausente e histórico fora do ar não cobram. Com entrega disponível, o pedido sem pagamento recebe 402; o pago recebe o resultado e cobranca.",
      "params": {
        "slug": {
          "desc": "Slug persistido do canal, vindo da coleção.",
          "exemplo": "axn"
        }
      },
      "query": {
        "programa": {
          "tipo": "string",
          "desc": "Title or category substring. Input and matches are not truncated."
        },
        "agenda": {
          "tipo": "string",
          "valores": [
            "1"
          ],
          "desc": "Include agenda_ics with the selected calendar events."
        }
      },
      "retorno": "ProgramacaoAvulsa",
      "exemploRetorno": {
        "canal": {
          "id": "AXN.br",
          "name": "AXN",
          "slug": "axn",
          "country": "BR",
          "timezones": "America/New_York",
          "playable_hint": 1,
          "categories": "movies",
          "languages": "por",
          "network": null,
          "website": "https://br.axn.com/",
          "logo_url": "https://gradetv.net/logos/AXN.br"
        },
        "dias": [
          {
            "dia": "2026-10-24",
            "programas": [
              {
                "inicio": "2026-10-24T05:40:00.000Z",
                "fim": "2026-10-24T06:30:00.000Z",
                "titulo": "Hudson & Rex",
                "desc": "Após um caminhão com explosivo líquido ser sequestrado por criminosos, Charlie e Rex correm contra o tempo para evitar uma explosão devastadora.",
                "categoria": "Temporada 4 Episódio 6 - Dead Man's Brid"
              }
            ]
          }
        ],
        "url": "https://gradetv.net/programacao/axn"
      },
      "erros": {
        "400": "Pago sem canal válido (`missing_input`) ou com data inválida (`invalid_date`), os dois com `example`. Não cobra.",
        "401": "Chave recusada (`invalid_api_key`), token OAuth recusado (`invalid_token`), crédito recusado (`credito_invalido`), crédito de teste fora do dev (`credito_simulado`) ou recuperação sem autenticação da carteira (`wallet_proof_required`). A primeira compra x402 não exige conta.",
        "402": "Sem pagamento: `accepts` (x402, USDC na Base) com o preço desta chamada, no mínimo US$ 0,01; `credito.preco_desta_chamada`, o preço menor com o crédito pré-pago; e `pacotes` (as recargas de crédito e quantas chamadas cada uma paga). Com o crédito sem saldo, `saldo_insuficiente`.",
        "404": "Canal ou dia ausente (`not_found`), ou programa sem correspondência nos dias coletados (`program_not_found`). Não cobra.",
        "409": "Pagamento em processamento, pedido diferente do original ou valor já recuperado no saldo. Não assine outra autorização para repetir a compra.",
        "410": "O prazo de 24 horas para recuperar a resposta acabou. O registro financeiro permanece; repetir a prova não cobra outra vez.",
        "503": "Histórico indisponível (`temporarily_unavailable`). Não cobra."
      },
      "exemplo": "curl -s -i $ORIGIN/api/x402/programacao/axn",
      "headers": {
        "authorization": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "`Bearer mmk_…` (chave de API) ou `Bearer mmo_…` (OAuth): a chamada paga usa o saldo da conta, sem outro cabeçalho. Sem conta, `Bearer cred_…` usa o crédito comprado em `POST /api/credito`."
        },
        "x-api-key": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "A chave de API `mmk_…` deste produto. Identifica a conta e autoriza usar o saldo na chamada paga."
        },
        "payment-signature": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "A prova x402 assinada (base64): `PAYMENT-SIGNATURE` (v2) ou `X-PAYMENT` (v1)."
        },
        "x-wallet-token": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "Para recuperar uma resposta x402 por até 24 horas, repita o mesmo pedido e a mesma prova. Envie aqui a credencial `mmw_…` da carteira pagadora, emitida por `/api/auth/wallet/agent/start` e `/api/auth/wallet/agent/token`. Não há nova cobrança."
        }
      },
      "agent_value": {
        "idioma": "pt",
        "en": {
          "when": "When does this programme air, and can I import its times into my calendar?",
          "result": "Programme matches across up to seven collected days, with a summary and optional importable calendar.",
          "replaces": "Select the title or category across collected days and receive calendar events with absolute times. You do not need to merge daily records and write a calendar exporter.",
          "free": "Read a day with GET /api/programacao/:slug/:dia. Check available dates and channel identity before choosing the weekly response.",
          "paid": "Use programa for title/category selection and agenda=1 for the calendar. The same per-call price uses x402 or credit. Missing programmes are not charged. Playback and chat passes are separate.",
          "limits": "Only collected dates are returned. This is metadata, not a video archive, a broadcast license or a guarantee that a channel plays.",
          "alternative": "One free day is enough, the needed dates are missing, or you already maintain the same program history."
        },
        "pt": {
          "when": "Quando este programa passa e posso importar os horários na agenda?",
          "result": "Exibições em até sete dias coletados, com resumo e agenda importável opcional.",
          "replaces": "Selecione título ou categoria nos dias coletados e receba eventos com horários absolutos. Você não precisa juntar os dias e criar um exportador de agenda.",
          "free": "Leia um dia em GET /api/programacao/:slug/:dia. Confira datas disponíveis e identidade do canal antes de escolher a semana.",
          "paid": "Use programa para selecionar título/categoria e agenda=1 para receber a agenda. O preço por chamada continua o mesmo, com x402 ou crédito. Programa ausente não cobra. TV e chat são serviços separados.",
          "limits": "Só retornam datas coletadas. São metadados, não arquivo de vídeo, licença de transmissão nem garantia de reprodução.",
          "alternative": "Um dia gratuito basta, faltam as datas necessárias ou você já mantém o mesmo histórico."
        }
      },
      "returns": "{ canal{id,name,slug,country,logo_url,categories,languages?,network?,website?,day?,url?,timezones?,playable_hint?}, dias[{dia,programas}], url, resumo, agenda_ics?, cobranca{via,preco_usd,repetido,saldo_usd?} }",
      "url": "https://gradetv.net/api/x402/programacao/:slug",
      "auth_detail": "Token de crédito em `Authorization: Bearer cred_…` (ou `X-Credito`). Para saldo recuperado de x402, use a credencial de carteira `mmw_…` em Bearer ou `X-Wallet-Token`, emitida por `/api/auth/wallet/agent/start` e `/api/auth/wallet/agent/token`."
    },
    {
      "auth": "credito",
      "method": "GET",
      "path": "/api/x402/programacao/:slug/:dia",
      "grupo": "Programação",
      "summary": "Sete dias de programação do canal que terminam no dia pedido, para o agente, pagos por chamada, sem conta.",
      "desc": "Até sete dias coletados do canal numa resposta, sem conta nem passe. programa seleciona por título/categoria sem caixa ou acento; agenda=1 inclui agenda_ics pronta para importar, com horários UTC e eventos únicos. O resumo informa dias consultados e exibições. A agenda é um arquivo, não uma assinatura de atualizações. A leitura diária gratuita conserva os mesmos dados. Canal, dia ou programa ausente e histórico fora do ar não cobram. Com entrega disponível, o pedido sem pagamento recebe 402; o pago recebe o resultado e cobranca.",
      "params": {
        "slug": {
          "desc": "Slug persistido do canal, vindo da coleção.",
          "exemplo": "axn"
        },
        "dia": {
          "desc": "Último dia da semana pedida, YYYY-MM-DD.",
          "exemplo": "2026-10-05"
        }
      },
      "query": {
        "programa": {
          "tipo": "string",
          "desc": "Title or category substring. Input and matches are not truncated."
        },
        "agenda": {
          "tipo": "string",
          "valores": [
            "1"
          ],
          "desc": "Include agenda_ics with the selected calendar events."
        }
      },
      "retorno": "ProgramacaoAvulsa",
      "exemploRetorno": {
        "canal": {
          "id": "AXN.br",
          "name": "AXN",
          "slug": "axn",
          "country": "BR",
          "timezones": "America/New_York",
          "playable_hint": 1,
          "categories": "movies",
          "languages": "por",
          "network": null,
          "website": "https://br.axn.com/",
          "logo_url": "https://gradetv.net/logos/AXN.br"
        },
        "dias": [
          {
            "dia": "2026-10-24",
            "programas": [
              {
                "inicio": "2026-10-24T05:40:00.000Z",
                "fim": "2026-10-24T06:30:00.000Z",
                "titulo": "Hudson & Rex",
                "desc": "Após um caminhão com explosivo líquido ser sequestrado por criminosos, Charlie e Rex correm contra o tempo para evitar uma explosão devastadora.",
                "categoria": "Temporada 4 Episódio 6 - Dead Man's Brid"
              }
            ]
          }
        ],
        "url": "https://gradetv.net/programacao/axn"
      },
      "erros": {
        "400": "Pago sem canal válido (`missing_input`) ou com data inválida (`invalid_date`), os dois com `example`. Não cobra.",
        "401": "Chave recusada (`invalid_api_key`), token OAuth recusado (`invalid_token`), crédito recusado (`credito_invalido`), crédito de teste fora do dev (`credito_simulado`) ou recuperação sem autenticação da carteira (`wallet_proof_required`). A primeira compra x402 não exige conta.",
        "402": "Sem pagamento: `accepts` (x402, USDC na Base) com o preço desta chamada, no mínimo US$ 0,01; `credito.preco_desta_chamada`, o preço menor com o crédito pré-pago; e `pacotes` (as recargas de crédito e quantas chamadas cada uma paga). Com o crédito sem saldo, `saldo_insuficiente`.",
        "404": "Canal ou dia ausente (`not_found`), ou programa sem correspondência nos dias coletados (`program_not_found`). Não cobra.",
        "409": "Pagamento em processamento, pedido diferente do original ou valor já recuperado no saldo. Não assine outra autorização para repetir a compra.",
        "410": "O prazo de 24 horas para recuperar a resposta acabou. O registro financeiro permanece; repetir a prova não cobra outra vez.",
        "503": "Histórico indisponível (`temporarily_unavailable`). Não cobra."
      },
      "exemplo": "curl -s -i $ORIGIN/api/x402/programacao/axn/2026-10-05",
      "headers": {
        "authorization": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "`Bearer mmk_…` (chave de API) ou `Bearer mmo_…` (OAuth): a chamada paga usa o saldo da conta, sem outro cabeçalho. Sem conta, `Bearer cred_…` usa o crédito comprado em `POST /api/credito`."
        },
        "x-api-key": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "A chave de API `mmk_…` deste produto. Identifica a conta e autoriza usar o saldo na chamada paga."
        },
        "payment-signature": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "A prova x402 assinada (base64): `PAYMENT-SIGNATURE` (v2) ou `X-PAYMENT` (v1)."
        },
        "x-wallet-token": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "Para recuperar uma resposta x402 por até 24 horas, repita o mesmo pedido e a mesma prova. Envie aqui a credencial `mmw_…` da carteira pagadora, emitida por `/api/auth/wallet/agent/start` e `/api/auth/wallet/agent/token`. Não há nova cobrança."
        }
      },
      "returns": "{ canal{id,name,slug,country,logo_url,categories,languages?,network?,website?,day?,url?,timezones?,playable_hint?}, dias[{dia,programas}], url, resumo, agenda_ics?, cobranca{via,preco_usd,repetido,saldo_usd?} }",
      "url": "https://gradetv.net/api/x402/programacao/:slug/:dia",
      "auth_detail": "Token de crédito em `Authorization: Bearer cred_…` (ou `X-Credito`). Para saldo recuperado de x402, use a credencial de carteira `mmw_…` em Bearer ou `X-Wallet-Token`, emitida por `/api/auth/wallet/agent/start` e `/api/auth/wallet/agent/token`."
    },
    {
      "method": "GET",
      "path": "/api/producers",
      "auth": "none",
      "grupo": "Produtores",
      "summary": "Oferta sob consulta para produtores com conteúdo autorizado e canais de contato.",
      "desc": "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 é livre — sem captcha nem pagamento —, uma mensagem a cada 10 s por rede.",
      "query": {
        "lang": {
          "tipo": "string",
          "desc": "Idioma da oferta: pt, en, es, fr, de ou hu; ausente ou desconhecido volta a pt.",
          "padrao": "pt",
          "valores": [
            "pt",
            "en",
            "es",
            "fr",
            "de",
            "hu"
          ]
        }
      },
      "retorno": {
        "status": {
          "tipo": "string",
          "desc": "`sob_consulta`: proposta sujeita a avaliação individual."
        },
        "activation_available": {
          "tipo": "bool",
          "desc": "Sempre false: não há ativação de transmissão nesta superfície."
        },
        "title": {
          "tipo": "string",
          "desc": "Nome da oferta no idioma solicitado."
        },
        "description": {
          "tipo": "string",
          "desc": "Apresentação do serviço sob consulta."
        },
        "audience": {
          "tipo": "string",
          "desc": "Perfil de produtores e organizações atendidos pela proposta."
        },
        "services": {
          "tipo": "string[]",
          "desc": "Capacidades a avaliar no projeto, sem compromisso de disponibilidade."
        },
        "requirements": {
          "tipo": "string",
          "desc": "Necessidade de autorização para sinal e obras, território e prazo."
        },
        "availability": {
          "tipo": "string",
          "desc": "Condição de avaliação antes de confirmar início e escopo."
        },
        "pricing": {
          "tipo": "string",
          "desc": "Orçamento sob consulta; enviar interesse não contrata o serviço."
        },
        "contact": {
          "tipo": "object",
          "desc": "email, form_url e api_url absolutos, message_template e instructions para apresentar canal/evento, direitos, audiência, duração e data."
        },
        "_links": {
          "tipo": "object",
          "desc": "self (esta API com idioma) e page (página da oferta): URLs absolutas."
        }
      },
      "erros": {
        "405": "A oferta só aceita GET; não existe provisionamento por POST."
      },
      "exemplo": "curl -s \"$ORIGIN/api/producers?lang=pt\"",
      "returns": "{ status, activation_available, title, description, audience, services, requirements, availability, pricing, contact, _links }",
      "url": "https://gradetv.net/api/producers",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/logos/:id",
      "auth": "none",
      "summary": "Logo do canal servido por nós, na variante de card (≤256px).",
      "grupo": "Mídia",
      "desc": "Nunca faz proxy na hora: ou o arquivo está no espelho de logos (os arquivos do app), ou responde 404. É o que impede a página de card virar um proxy de imagem de terceiro. Canal que saiu do catálogo responde 410, para a lista velha que ainda o cita parar de pedir; canal de conteúdo adulto responde 404, como o que não tem variante.",
      "params": {
        "id": {
          "desc": "ID do logo, que vem em `Canal.logo_url`.",
          "exemplo": "GloboNews.br"
        }
      },
      "retorno": {
        "_texto": "`image/webp` ou `image/png` — os bytes do logo."
      },
      "erros": {
        "404": "Não há variante em cache para este canal, ou o canal é de conteúdo adulto.",
        "410": "O canal saiu do catálogo: a lista que o citava está velha."
      },
      "returns": "`image/webp` ou `image/png` — os bytes do logo.",
      "url": "https://gradetv.net/logos/:id",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/s/:id",
      "auth": "none",
      "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.",
      "grupo": "Mídia",
      "desc": "É o endereço que aparece no M3U e na API: https://m3m8.gradetv.net/api/s/:id, com CORS * e no-store para a política por rede. HLS ao vivo tem 120 minutos grátis por rede/dia UTC; depois, mostra vídeo próprio com QR para o passe de 30, 90 ou 365 dias, a partir de 0,10 USDC. Links antigos em gradetv.net/api/s/ ainda 302 no-store para o mesmo path. A Grade não retransmite sinal de terceiros: os segmentos e chaves vêm da origem, no browser e no VLC. O player e o legado oferecem servidores por bandeira: o2 (m3m8.gradetv.net, padrão), o1 (br.gradetv.net), c5 (de.gradetv.net) e c6 (us.gradetv.net). Opções adicionais dependem da prontidão do serviço em /healthz. A troca preserva path/query, passe e franquia. Playlists internas usam o servidor escolhido 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. Sem redirecionamento para HTTP. Headers expostos por CORS: X-Grade-TV-Codigo (código do aviso), 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.",
      "params": {
        "id": {
          "desc": "ID do stream, que vem em `Stream.id`.",
          "exemplo": "1001Noites.br:SD:2168"
        }
      },
      "query": {
        "lista": {
          "tipo": "string",
          "desc": "Personal device UUID issued by the player. Copy the generated URL; the UUID is scoped to this channel and checked on each playlist request."
        },
        "faixa": {
          "tipo": "string",
          "valores": [
            "audio",
            "video"
          ],
          "desc": "Faixa HLS estável emitida pelo servidor; copie a URI completa."
        },
        "lingua": {
          "tipo": "string",
          "valores": [
            "en",
            "pt",
            "es",
            "fr",
            "de",
            "hu"
          ],
          "desc": "Idioma do aviso, antes do cookie e Accept-Language."
        },
        "p": {
          "tipo": "string",
          "desc": "Ticket de playlist interna emitido pelo hop, até 6000 caracteres, vinculado ao stream e à origem. Permanece válido enquanto origem/bloqueio permitirem."
        }
      },
      "retorno": {
        "_texto": "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."
      },
      "erros": {
        "404": "Stream bloqueado/ausente, ticket `p` inválido/expirado ou origem alterada.",
        "422": "`playlist_acesso`/`playlist_origem`/`playlist_limite` no hop do o2 (a CDN não reescreve 4xx).",
        "502": "`playlist_acesso` para 401/403 da origem (`upstream_status` no corpo); `playlist_origem` para outras falhas; `playlist_limite` para limite excedido.",
        "503": "Origem de playlists indisponível ou limite operacional atingido; `playlist_config` indica chave de assinatura indisponível."
      },
      "exemplo": "curl -Ls '$ORIGIN/api/s/STREAM_ID'",
      "returns": "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.",
      "url": "https://gradetv.net/api/s/:id",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/legacy/:id",
      "auth": "none",
      "summary": "Metadados públicos para o player legado HTTP, sem sessão.",
      "grupo": "Mídia",
      "desc": "O botão legacy_url abre http://legacy.gradetv.net/legacy?stream=ID, sem porta no endereço. A página isolada aceita lang=pt/en/es/fr/de/hu 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 banco.",
      "params": {
        "id": {
          "desc": "ID público do stream, incluindo compatibilidade de IDs antigos.",
          "exemplo": "1001Noites.br:SD:2168"
        }
      },
      "retorno": {
        "id": {
          "tipo": "string",
          "desc": "ID resolvido do stream."
        },
        "channel_id": {
          "tipo": "string",
          "desc": "ID do canal, para a ficha pública em GET /api/channels/:id."
        },
        "name": {
          "tipo": "string",
          "desc": "Nome do canal."
        },
        "kind": {
          "tipo": "string",
          "desc": "Tipo do stream para o player."
        },
        "radio": {
          "tipo": "bool",
          "desc": "Se o canal é rádio."
        },
        "url": {
          "tipo": "string",
          "desc": "URL pública do hop HTTPS de playlist."
        },
        "provider_url": {
          "tipo": "string",
          "desc": "URL direta da transmissão no provedor."
        },
        "website": {
          "tipo": "string",
          "desc": "Site oficial do canal; null quando ausente ou inválido. Destino do botão de abrir site, separado da URL da transmissão.",
          "nulo": true
        },
        "legacy_url": {
          "tipo": "string",
          "desc": "Player HTTP isolado, sem credencial."
        }
      },
      "erros": {
        "404": "Stream indisponível ou origem incompatível."
      },
      "exemplo": "curl -s '$ORIGIN/api/legacy/STREAM_ID'",
      "returns": "{ id, channel_id, name, kind, radio, url, provider_url, website, legacy_url }",
      "url": "https://gradetv.net/api/legacy/:id",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/m/:ticket",
      "auth": "none",
      "summary": "Desligado: era o pass-through de vídeo. Responde 410 sempre.",
      "grupo": "Mídia",
      "desc": "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.",
      "params": {
        "ticket": {
          "desc": "Ticket legado ignorado; nenhum valor reativa a retransmissão.",
          "exemplo": "tk_legado"
        }
      },
      "retorno": {
        "_texto": "410 `relay_desligado`, sempre."
      },
      "erros": {
        "404": "Caminho inexistente fora da família retirada.",
        "410": "Sempre: o Grade não retransmite vídeo."
      },
      "returns": "410 `relay_desligado`, sempre.",
      "url": "https://gradetv.net/api/m/:ticket",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/f/:token/library.:formato",
      "auth": "none",
      "summary": "Feed da biblioteca inteira do dono, no formato pedido pela extensão.",
      "grupo": "Feeds",
      "desc": "É a URL que a pessoa cola no app de IPTV da TV ou 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.",
      "params": {
        "token": {
          "desc": "Token de feed do dono; vem em `Biblioteca.feeds` e não é o guest token.",
          "exemplo": "k7m2p9r4t6v8w1y3z5b7c9d1"
        },
        "formato": {
          "desc": "Extensão que escolhe o formato de saída.",
          "valores": [
            "m3u",
            "m3u8",
            "json",
            "xspf",
            "xml"
          ],
          "exemplo": "m3u"
        }
      },
      "notes": [
        "Item do YouTube fica fora do M3U e do XSPF (o VLC não toca) e vem no JSON com `kind: \"youtube\"`.",
        "O M3U traz o guia da mesma lista no `x-tvg-url` e o id do canal no `tvg-id`. O `.xml` é esse guia em XMLTV: canal, horário, título e categoria do dia, sem sinopse."
      ],
      "retorno": {
        "_texto": "`audio/x-mpegurl` (m3u/m3u8), `application/json`, `application/xspf+xml` ou `application/xml` (xml, o guia XMLTV)."
      },
      "erros": {
        "404": "Token de feed desconhecido."
      },
      "exemplo": "curl -s $ORIGIN/f/FEED_TOKEN/library.m3u",
      "returns": "`audio/x-mpegurl` (m3u/m3u8), `application/json`, `application/xspf+xml` ou `application/xml` (xml, o guia XMLTV).",
      "url": "https://gradetv.net/f/:token/library.:formato",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/f/:token/c/:categoria.:formato",
      "auth": "none",
      "summary": "Feed de uma pasta da biblioteca, para assinar só aquele recorte.",
      "grupo": "Feeds",
      "params": {
        "token": {
          "desc": "Token de feed do dono, vindo de `Biblioteca.feeds`.",
          "exemplo": "k7m2p9r4t6v8w1y3z5b7c9d1"
        },
        "categoria": {
          "desc": "Slug da pasta, que vem em `PastaBiblioteca.slug`.",
          "exemplo": "jornalismo"
        },
        "formato": {
          "desc": "Extensão que escolhe o formato de saída.",
          "valores": [
            "m3u",
            "m3u8",
            "json",
            "xspf",
            "xml"
          ],
          "exemplo": "m3u"
        }
      },
      "notes": [
        "Item do YouTube fica fora do M3U e do XSPF (o VLC não toca) e vem no JSON com `kind: \"youtube\"`.",
        "O M3U traz o guia da mesma lista no `x-tvg-url` e o id do canal no `tvg-id`. O `.xml` é esse guia em XMLTV: canal, horário, título e categoria do dia, sem sinopse."
      ],
      "retorno": {
        "_texto": "`audio/x-mpegurl` (m3u/m3u8), `application/json`, `application/xspf+xml` ou `application/xml` (xml, o guia XMLTV)."
      },
      "erros": {
        "404": "Token de feed ou pasta desconhecidos."
      },
      "exemplo": "curl -s $ORIGIN/f/FEED_TOKEN/c/noticias.m3u",
      "returns": "`audio/x-mpegurl` (m3u/m3u8), `application/json`, `application/xspf+xml` ou `application/xml` (xml, o guia XMLTV).",
      "url": "https://gradetv.net/f/:token/c/:categoria.:formato",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/f/:token/c/:categoria/g/:grupo.:formato",
      "auth": "none",
      "summary": "Feed de uma sub-aba — o recorte mais fino que a galeria oferece.",
      "grupo": "Feeds",
      "params": {
        "token": {
          "desc": "Token de feed do dono, vindo de `Biblioteca.feeds`.",
          "exemplo": "k7m2p9r4t6v8w1y3z5b7c9d1"
        },
        "categoria": {
          "desc": "Slug da pasta que contém a sub-aba."
        },
        "grupo": {
          "desc": "Slug da sub-aba, que vem em `SubAba.slug`."
        },
        "formato": {
          "desc": "Extensão que escolhe o formato de saída.",
          "valores": [
            "m3u",
            "m3u8",
            "json",
            "xspf",
            "xml"
          ],
          "exemplo": "m3u"
        }
      },
      "notes": [
        "Item do YouTube fica fora do M3U e do XSPF (o VLC não toca) e vem no JSON com `kind: \"youtube\"`.",
        "O M3U traz o guia da mesma lista no `x-tvg-url` e o id do canal no `tvg-id`. O `.xml` é esse guia em XMLTV: canal, horário, título e categoria do dia, sem sinopse."
      ],
      "retorno": {
        "_texto": "`audio/x-mpegurl` (m3u/m3u8), `application/json`, `application/xspf+xml` ou `application/xml` (xml, o guia XMLTV)."
      },
      "erros": {
        "404": "Token de feed, pasta ou sub-aba desconhecidos."
      },
      "exemplo": "curl -s $ORIGIN/f/FEED_TOKEN/c/noticias/g/manchete.json",
      "returns": "`audio/x-mpegurl` (m3u/m3u8), `application/json`, `application/xspf+xml` ou `application/xml` (xml, o guia XMLTV).",
      "url": "https://gradetv.net/f/:token/c/:categoria/g/:grupo.:formato",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/l/:codigo",
      "auth": "none",
      "summary": "O M3U da biblioteca num link curto, para digitar no controle da TV.",
      "grupo": "Feeds",
      "desc": "O mesmo M3U de `/f/:token/library.m3u`, num endereço que se digita no controle da TV. O código são os 10 primeiros caracteres do token de feed e vem pronto em `Biblioteca.feeds.curto`; quem tem o link tem o conteúdo. `/l/:codigo/:pasta` e `/l/:codigo/:pasta/:subaba` são os M3U da pasta e da sub-aba. Sem prefixo, os textos saem em português; `/en/l/:codigo` e as outras línguas da Grade (`es`, `fr`, `de`, `hu`) trocam a língua.",
      "params": {
        "codigo": {
          "desc": "Os 10 primeiros caracteres do token de feed, que vêm em `Biblioteca.feeds.curto`.",
          "exemplo": "k7m2p9r4t6"
        }
      },
      "notes": [
        "Item do YouTube fica fora do M3U e do XSPF (o VLC não toca) e vem no JSON com `kind: \"youtube\"`.",
        "O M3U traz o guia da mesma lista no `x-tvg-url` e o id do canal no `tvg-id`. O `.xml` é esse guia em XMLTV: canal, horário, título e categoria do dia, sem sinopse."
      ],
      "retorno": {
        "_texto": "`audio/x-mpegurl`."
      },
      "erros": {
        "404": "Código desconhecido, ou o começo de mais de um token."
      },
      "exemplo": "curl -s $ORIGIN/l/k7m2p9r4t6",
      "returns": "`audio/x-mpegurl`.",
      "url": "https://gradetv.net/l/:codigo",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/pay",
      "auth": "none",
      "grupo": "TV",
      "summary": "Abre o pagamento do passe com o código exibido na TV.",
      "query": {
        "codigo": {
          "desc": "Seis dígitos exibidos no aviso da TV; validade de dez minutos.",
          "exemplo": "123456",
          "opcional": true
        },
        "lingua": {
          "tipo": "string",
          "valores": [
            "en",
            "pt",
            "es",
            "fr",
            "de",
            "hu"
          ],
          "desc": "Idioma da página; sem ele, preferência detectada."
        }
      },
      "exemplo": "curl -i \"$ORIGIN/pay?codigo=123456&lingua=pt\"",
      "retorno": {
        "_texto": "HTML: código, preço e Paywall global; confirmação após pagar."
      },
      "erros": {
        "429": "Limite de códigos distintos; repita após Retry-After.",
        "503": "Ativação indisponível."
      },
      "returns": "HTML: código, preço e Paywall global; confirmação após pagar.",
      "url": "https://gradetv.net/pay",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/tv/tela.js",
      "auth": "none",
      "grupo": "TV",
      "summary": "Carrega o comportamento global da tela de ativação.",
      "retorno": {
        "_texto": "JavaScript da ilha AtivaTv, Paywall e retomada HLS."
      },
      "erros": {
        "404": "Tela de ativação não configurada."
      },
      "returns": "JavaScript da ilha AtivaTv, Paywall e retomada HLS.",
      "url": "https://gradetv.net/api/tv/tela.js",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/tv/:codigo/estado",
      "auth": "none",
      "grupo": "TV",
      "summary": "Consulta a ativação da TV sem consumir minutos.",
      "params": {
        "codigo": {
          "desc": "Seis dígitos exibidos no aviso da TV; validade de dez minutos.",
          "exemplo": "123456"
        }
      },
      "retorno": {
        "ativo": {
          "tipo": "bool",
          "desc": "There is a valid pass for the identified TV device, or its legacy network."
        },
        "retomar": {
          "tipo": "string",
          "nulo": true,
          "desc": "Hop do canal que estava aberto na TV."
        }
      },
      "erros": {
        "404": "Rota não reconhecida.",
        "410": "Código inválido ou vencido.",
        "429": "Limite de códigos distintos; repita após Retry-After.",
        "503": "Consulta indisponível."
      },
      "returns": "{ ativo, retomar }",
      "url": "https://gradetv.net/api/tv/:codigo/estado",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/tv/:codigo/ws",
      "auth": "none",
      "grupo": "TV",
      "summary": "Acompanha a confirmação pelo WebSocket.",
      "params": {
        "codigo": {
          "desc": "Seis dígitos exibidos no aviso da TV; validade de dez minutos.",
          "exemplo": "123456"
        }
      },
      "desc": "Envie {\"tipo\":\"acompanhar\"} após abrir o socket. O servidor envia {\"ativo\":boolean,\"retomar\":string|null}. A gêmea HTTP é GET /api/tv/:codigo/estado. Mesmo domínio; até a validade do código.",
      "retorno": {
        "_texto": "Upgrade WebSocket; estado inicial e mudança após a compra."
      },
      "erros": {
        "403": "Origem recusada.",
        "404": "Rota não reconhecida.",
        "410": "Código vencido.",
        "429": "Limite de códigos distintos; repita após Retry-After.",
        "503": "Canal indisponível ou ocupado."
      },
      "returns": "Upgrade WebSocket; estado inicial e mudança após a compra.",
      "url": "https://gradetv.net/api/tv/:codigo/ws",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/tv/:codigo/comprar",
      "auth": "none",
      "grupo": "TV",
      "summary": "Buy a TV pass for 2 devices: 0.10 USDC for 30 days, 0.29 for 90 or 0.99 for 365.",
      "params": {
        "codigo": {
          "desc": "Seis dígitos exibidos no aviso da TV; validade de dez minutos.",
          "exemplo": "123456"
        }
      },
      "query": {
        "plano": {
          "desc": "O prazo do passe; sem ele, 30 dias.",
          "valores": [
            "mes",
            "trimestre",
            "ano"
          ],
          "exemplo": "trimestre"
        }
      },
      "desc": "No automatic renewal. A 402 uses x402 on Base; repeat the same call with PAYMENT-SIGNATURE (v2) or X-PAYMENT (v1). The shared ledger records the attempt, payment and entitlement. The code identifies the TV device, with the network as fallback. A verified payer account groups its devices. A full account receives an authorization code before payment. Reusing the receipt does not buy another pass.",
      "exemplo": "curl -i -X POST \"$ORIGIN/api/tv/123456/comprar?plano=trimestre\"",
      "retorno": {
        "ativo": {
          "tipo": "bool",
          "desc": "There is a valid pass for the identified TV device, or its legacy network."
        },
        "retomar": {
          "tipo": "string",
          "nulo": true,
          "desc": "Hop do canal que estava aberto na TV."
        },
        "valido_ate": {
          "tipo": "string",
          "desc": "Fim do prazo comprado, em UTC."
        }
      },
      "erros": {
        "402": "Pagamento necessário ou recusado.",
        "404": "Prazo do passe desconhecido (`tv_plano_invalido`) ou rota não reconhecida.",
        "409": "Device authorization is required; use the returned eight-digit code.",
        "410": "Código inválido ou vencido.",
        "429": "Limite de códigos distintos; repita após Retry-After.",
        "503": "Pagamento indisponível."
      },
      "returns": "{ ativo, retomar, valido_ate }",
      "url": "https://gradetv.net/api/tv/:codigo/comprar",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/tv/:codigo/aviso.:formato",
      "auth": "none",
      "grupo": "TV",
      "summary": "Imagem ou vídeo da Grade com QR e código de pagamento.",
      "params": {
        "codigo": {
          "desc": "Seis dígitos exibidos no aviso da TV; validade de dez minutos.",
          "exemplo": "123456"
        },
        "formato": {
          "desc": "Formato do aviso próprio.",
          "valores": [
            "png",
            "mp4",
            "ts",
            "audio.ts",
            "m3u8"
          ]
        }
      },
      "query": {
        "faixa": {
          "tipo": "string",
          "valores": [
            "audio",
            "video"
          ],
          "desc": "Faixa interna da lista HLS estável; copie a URI emitida."
        }
      },
      "exemplo": "curl -i \"$ORIGIN/api/tv/123456/aviso.m3u8?faixa=video\"",
      "desc": "The notice uses the selected language, a QR for /pay and six digits. A full account sees a device authorization request instead of a price. The server reuses silent audio for this first-party notice. Playback resumes for the authorized device; the code does not authenticate an account. Third-party media stays at its source.",
      "retorno": {
        "_texto": "PNG, MP4, MPEG-TS ou HLS; sem cache compartilhado. No domínio principal, 307 para o hop."
      },
      "erros": {
        "404": "Rota ou formato não reconhecido.",
        "410": "Código inválido ou vencido.",
        "422": "A emissora não entregou uma lista válida; tente outro espelho.",
        "429": "Limite de códigos distintos; repita após Retry-After.",
        "503": "Aviso indisponível ou orçamento atingido."
      },
      "returns": "PNG, MP4, MPEG-TS ou HLS; sem cache compartilhado. No domínio principal, 307 para o hop.",
      "url": "https://gradetv.net/api/tv/:codigo/aviso.:formato",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/tv/hop/playlist",
      "auth": "tv_hop",
      "grupo": "TV",
      "summary": "Capacidade privada do servidor de listas para consultar franquia e passe.",
      "query": {
        "caminho": {
          "tipo": "string",
          "desc": "Path/query exatos de /api/s/ validados pelo hop."
        },
        "servidor": {
          "tipo": "string",
          "valores": [
            "o2",
            "o1",
            "c5",
            "c6"
          ],
          "opcional": true,
          "desc": "Id do servidor cadastrado, incluído na assinatura. Sem ele, o2."
        }
      },
      "exemplo": "curl -i -H \"X-MM-TV-Hop: $TV_HOP\" \"$ORIGIN/api/tv/hop/playlist?caminho=%2Fapi%2Fs%2FTVBrasil.br%3ASD%3A15432.m3u8\"",
      "desc": "X-MM-TV-Hop: HMAC vinculado ao método, URL, rede e prazo de 15 s. Só o servidor de listas chama; nenhum pagamento roda nele. Sem assinatura, 403 antes de consultar a franquia.",
      "retorno": {
        "_texto": "HLS personalizado ou resposta de erro, sempre no-store."
      },
      "erros": {
        "400": "Caminho inválido.",
        "403": "Assinatura inválida.",
        "404": "Canal não reconhecido.",
        "422": "Lista da emissora indisponível, com código e dados da origem.",
        "503": "Serviço indisponível ou ocupado; respeite Retry-After."
      },
      "returns": "HLS personalizado ou resposta de erro, sempre no-store.",
      "url": "https://gradetv.net/api/tv/hop/playlist",
      "auth_detail": "Capacidade privada HMAC X-MM-TV-Hop, ligada ao pedido e válida por 15 segundos."
    },
    {
      "method": "POST",
      "path": "/api/tv/dispositivo",
      "auth": "none",
      "grupo": "TV",
      "summary": "Initialize the browser's playback cookie.",
      "headers": {
        "X-MM-Dispositivo": {
          "desc": "Required value: 1."
        },
        "Origin": {
          "desc": "An approved player origin."
        }
      },
      "desc": "Use credentials: include. Secure, HttpOnly cookie for up to 180 days. It does not authenticate an account. OPTIONS supports the isolated legacy player.",
      "retorno": {
        "_texto": "204; Set-Cookie only when a new identity is required."
      },
      "erros": {
        "400": "Invalid request or device code.",
        "401": "A device cookie or verified account is required.",
        "403": "Origin, CSRF or Turnstile verification failed.",
        "409": "Device authorization or a free seat is required.",
        "429": "Issuance limit reached.",
        "503": "Device service unavailable."
      },
      "returns": "204; Set-Cookie only when a new identity is required.",
      "url": "https://gradetv.net/api/tv/dispositivo",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/tv/dispositivo/config",
      "auth": "none",
      "grupo": "TV",
      "summary": "Read the public Turnstile site key.",
      "retorno": {
        "sitekey": {
          "tipo": "string",
          "desc": "Public widget key. Never the verification secret."
        }
      },
      "erros": {
        "503": "Turnstile is not configured."
      },
      "returns": "{ sitekey }",
      "url": "https://gradetv.net/api/tv/dispositivo/config",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/tv/dispositivo/lista",
      "auth": "device",
      "grupo": "TV",
      "summary": "Generate a private URL for one HLS channel and device.",
      "body": {
        "stream": {
          "tipo": "string",
          "desc": "Catalogue stream ID."
        },
        "servidor": {
          "tipo": "string",
          "valores": [
            "o2",
            "o1",
            "c5",
            "c6"
          ],
          "desc": "List server; default o2."
        },
        "turnstile": {
          "tipo": "string",
          "desc": "Single-use token for action playlist on the current hostname."
        }
      },
      "desc": "Same-origin JSON POST. Requires the playback cookie. Reuses the URL for the same device and channel. Limit: 20 attempts/hour per network and device, 5,000/day overall. A full account returns an authorization code, not a payment request.",
      "exemplo": "curl -X POST \"$ORIGIN/api/tv/dispositivo/lista\" -b cookies.txt -H \"Origin: $ORIGIN\" -H \"Content-Type: application/json\" -d '{\"stream\":\"STREAM_ID\",\"turnstile\":\"BROWSER_TOKEN\",\"servidor\":\"o2\"}'",
      "retorno": {
        "url": {
          "tipo": "string",
          "desc": "Private /s/:uuid URL. Sharing it shares this device's access."
        }
      },
      "erros": {
        "400": "Invalid request or device code.",
        "401": "A device cookie or verified account is required.",
        "403": "Origin, CSRF or Turnstile verification failed.",
        "409": "Device authorization or a free seat is required.",
        "429": "Issuance limit reached.",
        "503": "Device service unavailable."
      },
      "returns": "{ url }",
      "url": "https://gradetv.net/api/tv/dispositivo/lista",
      "auth_detail": "Signed HttpOnly playback cookie. Account writes also require verified account credentials and the SDK's same-origin CSRF protection."
    },
    {
      "method": "GET",
      "path": "/s/:uuid",
      "auth": "none",
      "grupo": "TV",
      "summary": "Open a personal channel URL in an external player.",
      "params": {
        "uuid": {
          "tipo": "string",
          "desc": "Opaque UUID returned by the generation endpoint."
        }
      },
      "query": {
        "servidor": {
          "tipo": "string",
          "valores": [
            "o2",
            "o1",
            "c5",
            "c6"
          ],
          "desc": "List server; default o2."
        }
      },
      "exemplo": "curl -L \"$ORIGIN/s/GENERATED_UUID?servidor=o2\"",
      "desc": "The UUID is a playback credential. The redirect and child playlists preserve it. The channel, authorization and pass are checked on playlist requests. No account data is returned.",
      "retorno": {
        "_texto": "302 to the selected list server; no-store."
      },
      "erros": {
        "400": "Unknown server.",
        "404": "Unknown UUID."
      },
      "returns": "302 to the selected list server; no-store.",
      "url": "https://gradetv.net/s/:uuid",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/tv/dispositivo/estado",
      "auth": "none",
      "grupo": "TV",
      "summary": "Read the current account's device capacity and offers.",
      "retorno": {
        "conta": {
          "tipo": "bool",
          "desc": "Whether the request has a verified account."
        },
        "grupo": {
          "tipo": "object",
          "nulo": true,
          "desc": "This account's device group, when associated."
        },
        "limite": {
          "tipo": "int",
          "desc": "Current device capacity."
        },
        "passe": {
          "tipo": "bool",
          "desc": "Whether the group has an active base pass."
        },
        "ofertas": {
          "tipo": "array"
        }
      },
      "erros": {
        "400": "Invalid request or device code.",
        "401": "A device cookie or verified account is required.",
        "403": "Origin, CSRF or Turnstile verification failed.",
        "409": "Device authorization or a free seat is required.",
        "429": "Issuance limit reached.",
        "503": "Device service unavailable."
      },
      "returns": "{ conta, grupo, limite, passe, ofertas }",
      "url": "https://gradetv.net/api/tv/dispositivo/estado",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/tv/dispositivo/vincular",
      "auth": "device",
      "grupo": "TV",
      "summary": "Associate this browser with its verified account.",
      "body": {},
      "desc": "Same-origin JSON POST. Existing associations do not write. Full accounts keep the device pending.",
      "exemplo": "curl -X POST \"$ORIGIN/api/tv/dispositivo/vincular\" -b cookies.txt -H \"Origin: $ORIGIN\" -H \"X-CSRF-Token: SESSION_CSRF\" -H \"Content-Type: application/json\" -d '{}'",
      "retorno": {
        "vinculado": {
          "tipo": "bool",
          "desc": "Whether the device belongs to the verified account."
        },
        "ativo": {
          "tipo": "bool",
          "desc": "Whether this device has an active seat."
        }
      },
      "erros": {
        "400": "Invalid request or device code.",
        "401": "A device cookie or verified account is required.",
        "403": "Origin, CSRF or Turnstile verification failed.",
        "409": "Device authorization or a free seat is required.",
        "429": "Issuance limit reached.",
        "503": "Device service unavailable."
      },
      "returns": "{ vinculado, ativo }",
      "url": "https://gradetv.net/api/tv/dispositivo/vincular",
      "auth_detail": "Signed HttpOnly playback cookie. Account writes also require verified account credentials and the SDK's same-origin CSRF protection."
    },
    {
      "method": "POST",
      "path": "/api/tv/dispositivo/autorizar",
      "auth": "session",
      "grupo": "TV",
      "summary": "Authorize a device or replace an account device without charge.",
      "exemplo": "curl -X POST \"$ORIGIN/api/tv/dispositivo/autorizar\" -b cookies.txt -H \"Origin: $ORIGIN\" -H \"X-CSRF-Token: SESSION_CSRF\" -H \"Content-Type: application/json\" -d '{\"codigo\":\"DEVICE_CODE\"}'",
      "body": {
        "codigo": {
          "tipo": "string",
          "desc": "Eight digits; expires in ten minutes; one use."
        },
        "substituir": {
          "tipo": "string",
          "desc": "Optional ID of this account's device to replace."
        }
      },
      "retorno": {
        "ativo": {
          "tipo": "bool",
          "desc": "Whether this device has an active seat."
        },
        "id": {
          "tipo": "string",
          "desc": "Authorized device ID."
        },
        "limite": {
          "tipo": "int",
          "desc": "Current device capacity."
        },
        "grupo": {
          "tipo": "object",
          "desc": "Updated account device group."
        }
      },
      "erros": {
        "400": "Invalid request or device code.",
        "401": "A device cookie or verified account is required.",
        "403": "Origin, CSRF or Turnstile verification failed.",
        "409": "Device authorization or a free seat is required.",
        "429": "Issuance limit reached.",
        "503": "Device service unavailable."
      },
      "returns": "{ ativo, id, limite, grupo }",
      "url": "https://gradetv.net/api/tv/dispositivo/autorizar",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/tv/dispositivo/ampliar",
      "auth": "session",
      "grupo": "TV",
      "summary": "Buy a 30-day device bundle or one extra seat.",
      "exemplo": "curl -X POST \"$ORIGIN/api/tv/dispositivo/ampliar\" -b cookies.txt -H \"Origin: $ORIGIN\" -H \"X-CSRF-Token: SESSION_CSRF\" -H \"Content-Type: application/json\" -d '{\"oferta\":\"cinco\",\"pedido\":\"REQUEST_UUID\"}'",
      "body": {
        "oferta": {
          "tipo": "string",
          "valores": [
            "cinco",
            "dez",
            "quinze",
            "avulso"
          ]
        },
        "pedido": {
          "tipo": "string",
          "desc": "Opaque UUID returned by the generation endpoint."
        }
      },
      "desc": "5/10/15 devices cost 0.20/0.35/0.50 USDC. One extra seat costs 0.05 USDC and requires an active pass. Maximum 15. No automatic renewal. Repeat the identical body and request UUID after the shared 402 payment flow. The verified account supplies the owner; the client cannot choose it.",
      "retorno": {
        "ativo": {
          "tipo": "bool",
          "desc": "Whether the purchase is active."
        },
        "limite": {
          "tipo": "int",
          "desc": "Current device capacity."
        },
        "valido_ate": {
          "tipo": "string",
          "desc": "Purchase expiry in UTC, when a new entitlement was recorded."
        }
      },
      "erros": {
        "400": "Invalid request or device code.",
        "401": "A device cookie or verified account is required.",
        "402": "Payment required.",
        "403": "Origin, CSRF or Turnstile verification failed.",
        "409": "Device authorization or a free seat is required.",
        "429": "Issuance limit reached.",
        "503": "Device service unavailable."
      },
      "returns": "{ ativo, limite, valido_ate }",
      "url": "https://gradetv.net/api/tv/dispositivo/ampliar",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/guest",
      "auth": "none",
      "summary": "Cria um convidado `ipt_…` — é a identidade que guarda galeria, histórico e favoritos sem conta.",
      "grupo": "Identidade",
      "desc": "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.",
      "retorno": {
        "token": {
          "tipo": "string",
          "desc": "O convidado, prefixo `ipt_`. Mande em `X-Guest-Token` ou como Bearer."
        }
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/guest",
      "returns": "{ token }",
      "url": "https://gradetv.net/api/guest",
      "auth_detail": "Público, sem credencial."
    },
    {
      "auth": "none",
      "grupo": "Identidade",
      "desc": "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.",
      "retorno": {
        "_texto": "410 `api_keys_retired`, sempre."
      },
      "erros": {
        "410": "Sempre: as chaves de API próprias foram retiradas."
      },
      "method": "POST",
      "path": "/api/keys",
      "summary": "Retirada: criava chave `iptk_…`. Responde 410.",
      "exemplo": "curl -s -XPOST $ORIGIN/api/keys",
      "returns": "410 `api_keys_retired`, sempre.",
      "url": "https://gradetv.net/api/keys",
      "auth_detail": "Público, sem credencial."
    },
    {
      "auth": "none",
      "grupo": "Identidade",
      "desc": "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.",
      "retorno": {
        "_texto": "410 `api_keys_retired`, sempre."
      },
      "erros": {
        "410": "Sempre: as chaves de API próprias foram retiradas."
      },
      "method": "GET",
      "path": "/api/keys",
      "summary": "Retirada: listava as chaves `iptk_…`. Responde 410.",
      "exemplo": "curl -s $ORIGIN/api/keys",
      "returns": "410 `api_keys_retired`, sempre.",
      "url": "https://gradetv.net/api/keys",
      "auth_detail": "Público, sem credencial."
    },
    {
      "auth": "none",
      "grupo": "Identidade",
      "desc": "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.",
      "retorno": {
        "_texto": "410 `api_keys_retired`, sempre."
      },
      "erros": {
        "404": "Caminho fora da família retirada — todo `/api/keys/…` responde 410.",
        "410": "Sempre: as chaves de API próprias foram retiradas."
      },
      "method": "DELETE",
      "path": "/api/keys/:id",
      "summary": "Retirada: revogava uma chave `iptk_…`. Responde 410.",
      "params": {
        "id": {
          "desc": "ID de chave antiga; ignorado.",
          "exemplo": "key_9f3c2b1d7a"
        }
      },
      "exemplo": "curl -s -XDELETE $ORIGIN/api/keys/KEY_ID",
      "returns": "410 `api_keys_retired`, sempre.",
      "url": "https://gradetv.net/api/keys/:id",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/nsfw/pass",
      "auth": "none",
      "summary": "Retirada: lia o passe do conteúdo adulto. Responde 410.",
      "grupo": "Identidade",
      "desc": "O conteúdo adulto saiu da Grade em 17/09/2026 e não volta por passe, consentimento, flag nem ticket (`is_nsfw` fica como classificação interna, para excluir o canal de toda leitura).",
      "retorno": {
        "_texto": "410 `adult_content_removed`, sempre."
      },
      "erros": {
        "410": "Sempre: não há passe adulto."
      },
      "exemplo": "curl -s $ORIGIN/api/nsfw/pass",
      "returns": "410 `adult_content_removed`, sempre.",
      "url": "https://gradetv.net/api/nsfw/pass",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/nsfw/pass",
      "auth": "none",
      "summary": "Retirada: comprava o passe do conteúdo adulto. Responde 410.",
      "grupo": "Identidade",
      "desc": "O conteúdo adulto saiu da Grade em 17/09/2026 e não volta por passe, consentimento, flag nem ticket (`is_nsfw` fica como classificação interna, para excluir o canal de toda leitura).",
      "retorno": {
        "_texto": "410 `adult_content_removed`, sempre."
      },
      "erros": {
        "410": "Sempre: não há passe adulto."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/nsfw/pass",
      "returns": "410 `adult_content_removed`, sempre.",
      "url": "https://gradetv.net/api/nsfw/pass",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/library",
      "auth": "guest",
      "summary": "A galeria inteira do dono: pastas, sub-abas, canais e as URLs de feed de cada nível.",
      "grupo": "Galeria",
      "retorno": "Biblioteca",
      "erros": [
        401
      ],
      "exemplo": "curl -s $ORIGIN/api/library -H \"X-Guest-Token: $IPT\"",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://gradetv.net/api/library",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "POST",
      "path": "/api/categories",
      "auth": "guest",
      "summary": "Cria uma pasta na galeria, já com a sub-aba Geral dentro dela.",
      "grupo": "Galeria",
      "desc": "Teto de 8 pastas por dono. A resposta traz a biblioteca inteira já atualizada — não precisa recarregar `GET /api/library` depois.",
      "corpo": {
        "name": {
          "tipo": "string",
          "desc": "Nome da pasta, até 40 caracteres.",
          "obrigatorio": true
        }
      },
      "body": {
        "name": "Notícias"
      },
      "retorno": "BibliotecaComPasta",
      "erros": {
        "400": "Nome vazio, longo demais, ou o teto de 8 pastas foi atingido.",
        "401": null
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/categories -H \"X-Guest-Token: $IPT\" -H 'content-type: application/json' -d '{\"name\":\"Notícias\"}'",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, name, slug, group_id }",
      "url": "https://gradetv.net/api/categories",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "PATCH",
      "path": "/api/categories/:id",
      "auth": "guest",
      "summary": "Renomeia uma pasta. O slug do feed acompanha o nome novo.",
      "grupo": "Galeria",
      "desc": "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.",
      "params": {
        "id": {
          "desc": "ID da pasta, `cat_…`."
        }
      },
      "corpo": {
        "name": {
          "tipo": "string",
          "desc": "Novo nome da pasta, até 40 caracteres.",
          "obrigatorio": true
        }
      },
      "body": {
        "name": "Jornalismo"
      },
      "retorno": "Biblioteca",
      "erros": {
        "400": "Nome vazio ou longo demais.",
        "401": null,
        "404": null
      },
      "exemplo": "curl -s -XPATCH $ORIGIN/api/categories/cat_123 -H \"X-Guest-Token: $IPT\" -H 'content-type: application/json' -d '{\"name\":\"Jornalismo\"}'",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://gradetv.net/api/categories/:id",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "DELETE",
      "path": "/api/categories/:id",
      "auth": "guest",
      "summary": "Apaga a pasta e tudo que está dentro dela: sub-abas e canais.",
      "grupo": "Galeria",
      "params": {
        "id": {
          "desc": "ID da pasta, `cat_…`."
        }
      },
      "retorno": "Biblioteca",
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/categories/cat_123 -H \"X-Guest-Token: $IPT\"",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://gradetv.net/api/categories/:id",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "POST",
      "path": "/api/groups",
      "auth": "guest",
      "summary": "Cria uma sub-aba dentro de uma pasta.",
      "grupo": "Galeria",
      "desc": "Teto de 12 sub-abas por pasta. A resposta traz a biblioteca inteira já atualizada — não precisa recarregar `GET /api/library` depois.",
      "corpo": {
        "category_id": {
          "tipo": "string",
          "desc": "Pasta que vai receber a sub-aba, `cat_…`.",
          "obrigatorio": true
        },
        "name": {
          "tipo": "string",
          "desc": "Nome da sub-aba, até 40 caracteres.",
          "obrigatorio": true
        }
      },
      "body": {
        "category_id": "cat_…",
        "name": "Manchete"
      },
      "retorno": "BibliotecaComSubAba",
      "erros": {
        "400": "Nome inválido ou teto de 12 sub-abas atingido.",
        "401": null,
        "404": "A pasta não é sua ou não existe."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/groups -H \"X-Guest-Token: $IPT\" -H 'content-type: application/json' -d '{\"category_id\":\"cat_123\",\"name\":\"Manchete\"}'",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, name, slug, category_id }",
      "url": "https://gradetv.net/api/groups",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "DELETE",
      "path": "/api/groups/:id",
      "auth": "guest",
      "summary": "Apaga uma sub-aba e os canais que estavam nela.",
      "grupo": "Galeria",
      "params": {
        "id": {
          "desc": "ID da sub-aba, `grp_…`."
        }
      },
      "retorno": "Biblioteca",
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/groups/grp_123 -H \"X-Guest-Token: $IPT\"",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://gradetv.net/api/groups/:id",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "POST",
      "path": "/api/items",
      "auth": "guest",
      "summary": "Põe um canal do catálogo numa sub-aba da galeria.",
      "grupo": "Galeria",
      "desc": "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.",
      "corpo": {
        "group_id": {
          "tipo": "string",
          "desc": "Sub-aba que recebe o canal, `grp_…`.",
          "obrigatorio": true
        },
        "channel_id": {
          "tipo": "string",
          "desc": "ID do canal no catálogo, ex. `GloboNews.br`.",
          "obrigatorio": true
        },
        "stream_id": {
          "tipo": "string",
          "desc": "Stream específico; sem ele o servidor escolhe o melhor."
        }
      },
      "body": {
        "group_id": "grp_…",
        "channel_id": "GloboNews.br"
      },
      "retorno": "BibliotecaComItem",
      "erros": {
        "400": "Teto de 40 canais na sub-aba atingido.",
        "401": null,
        "404": "Sub-aba ou canal não encontrados."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/items -H \"X-Guest-Token: $IPT\" -H 'content-type: application/json' -d '{\"group_id\":\"grp_123\",\"channel_id\":\"GloboNews.br\"}'",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, group_id, category_id }",
      "url": "https://gradetv.net/api/items",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "DELETE",
      "path": "/api/items/:id",
      "auth": "guest",
      "summary": "Tira um canal da sub-aba. O canal continua no catálogo público, claro.",
      "grupo": "Galeria",
      "params": {
        "id": {
          "desc": "ID do item na galeria, `itm_…`."
        }
      },
      "retorno": "Biblioteca",
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/items/itm_123 -H \"X-Guest-Token: $IPT\"",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://gradetv.net/api/items/:id",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "POST",
      "path": "/api/items/:id/play",
      "auth": "guest",
      "summary": "Marca este canal como o último tocado da sub-aba — é o que devolve a pessoa onde parou.",
      "grupo": "Galeria",
      "desc": "Não conta play público nem entra no histórico; para isso são `POST /api/history` e `POST /api/play-report`.",
      "params": {
        "id": {
          "desc": "ID do item na galeria, `itm_…`."
        }
      },
      "retorno": "Biblioteca",
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s -XPOST $ORIGIN/api/items/itm_123/play -H \"X-Guest-Token: $IPT\"",
      "returns": "{ owner, feeds{m3u,m3u8,json,xspf,xml,curto}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://gradetv.net/api/items/:id/play",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "GET",
      "path": "/api/history",
      "auth": "guest",
      "summary": "Canais que o dono assistiu, do mais recente para o mais antigo.",
      "grupo": "Histórico",
      "desc": "Exibe somente canais disponíveis no catálogo.",
      "query": {
        "limit": {
          "tipo": "int",
          "desc": "Itens por página. Acima de 50 é silenciosamente reduzido a 50.",
          "padrao": 20
        },
        "offset": {
          "tipo": "int",
          "desc": "Quantos itens pular. Use `next_offset` da resposta anterior.",
          "padrao": 0
        }
      },
      "retorno": "PaginaDeHistorico",
      "erros": [
        401
      ],
      "exemplo": "curl -s '$ORIGIN/api/history?limit=10' -H \"X-Guest-Token: $IPT\"",
      "returns": "{ items[{channel_id,name,country,logo_url,playable_hint,plays,first_at,last_at,stale,api}], total, limit, offset, next_offset, next, api, max }",
      "url": "https://gradetv.net/api/history",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "POST",
      "path": "/api/history",
      "auth": "guest",
      "summary": "Registra que o dono assistiu um canal. Repetir soma em `plays` e sobe a linha.",
      "grupo": "Histórico",
      "corpo": {
        "channel_id": {
          "tipo": "string",
          "desc": "Canal assistido, ex. `GloboNews.br`.",
          "obrigatorio": true
        }
      },
      "body": {
        "channel_id": "GloboNews.br"
      },
      "retorno": {
        "channel_id": {
          "tipo": "string",
          "desc": "O canal registrado."
        },
        "plays": {
          "tipo": "int",
          "desc": "Quantas vezes o dono já assistiu este canal."
        },
        "last_at": {
          "tipo": "string",
          "desc": "Momento deste registro (UTC).",
          "nulo": true
        },
        "api": {
          "tipo": "string",
          "desc": "URL absoluta do histórico."
        }
      },
      "erros": {
        "400": "`channel_id` ausente ou JSON inválido.",
        "401": null
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/history -H \"X-Guest-Token: $IPT\" -H 'content-type: application/json' -d '{\"channel_id\":\"GloboNews.br\"}'",
      "returns": "{ channel_id, plays, last_at, api }",
      "url": "https://gradetv.net/api/history",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "DELETE",
      "path": "/api/history/:channel_id",
      "auth": "guest",
      "summary": "Tira um canal do histórico do dono.",
      "grupo": "Histórico",
      "params": {
        "channel_id": {
          "desc": "Canal a remover do histórico, ex. `GloboNews.br`."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true` quando removeu."
        },
        "removed": {
          "tipo": "string",
          "desc": "O `channel_id` que saiu."
        }
      },
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/history/GloboNews.br -H \"X-Guest-Token: $IPT\"",
      "returns": "{ ok, removed }",
      "url": "https://gradetv.net/api/history/:channel_id",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "DELETE",
      "path": "/api/history",
      "auth": "guest",
      "summary": "Limpa o histórico inteiro do dono, de uma vez.",
      "grupo": "Histórico",
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        },
        "cleared": {
          "tipo": "bool",
          "desc": "Sempre `true` — o histórico foi zerado."
        },
        "total": {
          "tipo": "int",
          "desc": "Quantos restaram: zero."
        }
      },
      "erros": [
        401
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/history -H \"X-Guest-Token: $IPT\"",
      "returns": "{ ok, cleared, total }",
      "url": "https://gradetv.net/api/history",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "POST",
      "path": "/api/play-report",
      "statusOk": [
        201,
        200
      ],
      "auth": "guest",
      "summary": "Informa se o canal reproduziu ou falhou.",
      "grupo": "Saúde",
      "desc": "O primeiro relato desse resultado no dia devolve 201. Conta até uma vez por dono, canal, dia e resultado. Repetir devolve 200 com `reason: ja_relatado_hoje`. Navegador e sistema vêm do User-Agent. O país vem dos dados do pedido. Esses campos não podem ser alterados pelo corpo. Os relatos ajudam a comparar resultados em `GET /api/channels/:id/health`, mas não confirmam a causa da falha.",
      "corpo": {
        "channel_id": {
          "tipo": "string",
          "desc": "Canal que você tentou assistir.",
          "obrigatorio": true
        },
        "ok": {
          "tipo": "bool",
          "desc": "`true` se tocou, `false` se falhou.",
          "obrigatorio": true
        },
        "code": {
          "tipo": "string",
          "desc": "Código da falha; só quando `ok` é `false`. Código ausente ou desconhecido entra como `outro`.",
          "valores": [
            "cors",
            "geo",
            "sumiu",
            "codec",
            "playlist",
            "sem_resposta",
            "protocolo",
            "sem_stream",
            "embed",
            "outro"
          ]
        }
      },
      "body": {
        "channel_id": "GloboNews.br",
        "ok": false,
        "code": "cors"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true` — o relato foi aceito."
        },
        "counted": {
          "tipo": "bool",
          "desc": "`false` quando você já tinha relatado o mesmo hoje."
        },
        "reason": {
          "tipo": "string",
          "desc": "Por que não contou; só aparece quando `counted` é `false`.",
          "opcional": true
        },
        "channel_id": {
          "tipo": "string",
          "desc": "O canal relatado."
        },
        "stats": {
          "tipo": "Social",
          "desc": "Os contadores do canal já com este relato dentro."
        },
        "health": {
          "tipo": "string",
          "desc": "URL do painel de saúde completo deste canal."
        }
      },
      "erros": {
        "400": "JSON inválido, `channel_id` ausente ou `ok` diferente de `true` ou `false`.",
        "401": null,
        "404": "Canal não encontrado no catálogo."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/play-report -H \"X-Guest-Token: $IPT\" -H 'content-type: application/json' -d '{\"channel_id\":\"GloboNews.br\",\"ok\":false,\"code\":\"geo\"}'",
      "returns": "{ 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 }",
      "url": "https://gradetv.net/api/play-report",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "GET",
      "path": "/api/play-reports",
      "auth": "token",
      "summary": "Consulta relatos individuais de reprodução. Acesso exclusivo do operador.",
      "grupo": "Saúde",
      "desc": "Nos relatos atuais, o campo `ip` contém um identificador calculado a partir da rede e de um valor secreto. O prazo de retenção está em `retention_days`. O painel público não devolve esse identificador.",
      "query": {
        "channel_id": {
          "tipo": "string",
          "desc": "Restringe a um canal.",
          "exemplo": "GloboNews.br"
        },
        "ok": {
          "tipo": "bool",
          "desc": "`0` traz só as falhas — é o recorte que interessa numa investigação.",
          "valores": [
            "0"
          ]
        },
        "limit": {
          "tipo": "int",
          "desc": "Itens por página. Acima de 50 é silenciosamente reduzido a 50.",
          "padrao": 20
        },
        "offset": {
          "tipo": "int",
          "desc": "Quantos itens pular. Use `next_offset` da resposta anterior.",
          "padrao": 0
        }
      },
      "retorno": "PaginaDeRelatos",
      "erros": [
        401
      ],
      "exemplo": "curl -s '$ORIGIN/api/play-reports?channel_id=GloboNews.br&ok=0' -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "{ items[{channel_id,ok,code,ip,browser,os,country,day,at}], limit, offset, next_offset, retention_days, filters{channel_id,only_failures}, api }",
      "url": "https://gradetv.net/api/play-reports",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "GET",
      "path": "/api/favorites",
      "auth": "guest",
      "summary": "Canais favoritados pelo dono, do mais recente para o mais antigo.",
      "grupo": "Favoritos",
      "desc": "Exibe somente canais disponíveis no catálogo.",
      "query": {
        "limit": {
          "tipo": "int",
          "desc": "Itens por página. Acima de 50 é silenciosamente reduzido a 50.",
          "padrao": 20
        },
        "offset": {
          "tipo": "int",
          "desc": "Quantos itens pular. Use `next_offset` da resposta anterior.",
          "padrao": 0
        }
      },
      "retorno": "PaginaDeFavoritos",
      "erros": [
        401
      ],
      "exemplo": "curl -s '$ORIGIN/api/favorites?limit=10' -H \"X-Guest-Token: $IPT\"",
      "returns": "{ items[{channel_id,name,country,quality,logo_url,playable_hint,favorites,created_at,stale,api}], total, limit, offset, next_offset, next, api, max }",
      "url": "https://gradetv.net/api/favorites",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "POST",
      "path": "/api/favorites",
      "auth": "guest",
      "summary": "Favorita um canal. Repetir não soma: o contador público conta pessoas, não cliques.",
      "grupo": "Favoritos",
      "corpo": {
        "channel_id": {
          "tipo": "string",
          "desc": "Canal a favoritar, ex. `GloboNews.br`.",
          "obrigatorio": true
        }
      },
      "body": {
        "channel_id": "GloboNews.br"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        },
        "favorited": {
          "tipo": "bool",
          "desc": "Sempre `true` ao fim desta chamada."
        },
        "created": {
          "tipo": "bool",
          "desc": "`true` se foi agora; `false` se já era favorito."
        },
        "channel_id": {
          "tipo": "string",
          "desc": "O canal favoritado."
        },
        "favorites": {
          "tipo": "int",
          "desc": "Total de pessoas que favoritaram este canal."
        }
      },
      "erros": {
        "400": "`channel_id` ausente ou teto de favoritos atingido.",
        "401": null
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/favorites -H \"X-Guest-Token: $IPT\" -H 'content-type: application/json' -d '{\"channel_id\":\"GloboNews.br\"}'",
      "returns": "{ ok, favorited, created, channel_id, favorites }",
      "url": "https://gradetv.net/api/favorites",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "DELETE",
      "path": "/api/favorites/:channel_id",
      "auth": "guest",
      "summary": "Desfavorita o canal e devolve o ponto ao contador público.",
      "grupo": "Favoritos",
      "params": {
        "channel_id": {
          "desc": "Canal a desfavoritar, ex. `GloboNews.br`."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        },
        "favorited": {
          "tipo": "bool",
          "desc": "Sempre `false` ao fim desta chamada."
        },
        "channel_id": {
          "tipo": "string",
          "desc": "O canal que saiu dos favoritos."
        },
        "favorites": {
          "tipo": "int",
          "desc": "Total de pessoas que ainda favoritam este canal."
        }
      },
      "erros": {
        "401": null,
        "404": "O canal não estava nos seus favoritos."
      },
      "exemplo": "curl -s -XDELETE $ORIGIN/api/favorites/GloboNews.br -H \"X-Guest-Token: $IPT\"",
      "returns": "{ ok, favorited, channel_id, favorites }",
      "url": "https://gradetv.net/api/favorites/:channel_id",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "GET",
      "path": "/api/channels/:id/comments",
      "auth": "none",
      "summary": "Comentários públicos de um canal, do mais novo para o mais antigo.",
      "grupo": "Comentários",
      "desc": "Com credencial na chamada, cada comentário seu vem com `mine: true` — é assim que a interface sabe o que dá para apagar.",
      "params": {
        "id": {
          "desc": "ID do canal no catálogo.",
          "exemplo": "GloboNews.br"
        }
      },
      "query": {
        "limit": {
          "tipo": "int",
          "desc": "Itens por página. Acima de 50 é silenciosamente reduzido a 50.",
          "padrao": 20
        },
        "offset": {
          "tipo": "int",
          "desc": "Quantos itens pular. Use `next_offset` da resposta anterior.",
          "padrao": 0
        }
      },
      "retorno": "PaginaDeComentarios",
      "erros": {
        "404": "Canal não existe no catálogo."
      },
      "exemplo": "curl -s '$ORIGIN/api/channels/GloboNews.br/comments?limit=10'",
      "returns": "{ items[{id,channel_id,author,body,created_at,mine,api}], total, limit, offset, next_offset, next, api, channel_id, max_length }",
      "url": "https://gradetv.net/api/channels/:id/comments",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/channels/:id/comments",
      "auth": "guest",
      "summary": "Escreve um comentário no canal. Teto de 20 por hora por dono.",
      "grupo": "Comentários",
      "desc": "Sem `author`, o apelido é gerado e fica estável para o mesmo dono — a pessoa não vira um nome diferente a cada mensagem.",
      "params": {
        "id": {
          "desc": "ID do canal no catálogo.",
          "exemplo": "GloboNews.br"
        }
      },
      "corpo": {
        "body": {
          "tipo": "string",
          "desc": "O texto do comentário; o teto vem em `max_length` da listagem.",
          "obrigatorio": true
        },
        "author": {
          "tipo": "string",
          "desc": "Apelido a usar; sem ele o servidor gera um estável."
        }
      },
      "body": {
        "body": "Só abre no formato 720p.",
        "author": "Wendel"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true` quando o comentário entrou."
        },
        "comment": {
          "tipo": "Comentario",
          "desc": "O comentário criado, do jeito que ele aparece na listagem."
        }
      },
      "erros": {
        "400": "Texto vazio ou acima de `max_length`.",
        "401": null,
        "404": "Canal não existe.",
        "429": "Passou de 20 comentários na hora."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/channels/GloboNews.br/comments -H \"X-Guest-Token: $IPT\" -H 'content-type: application/json' -d '{\"body\":\"Só abre em 720p.\"}'",
      "returns": "{ ok, comment{id,channel_id,author,body,created_at,mine,api} }",
      "url": "https://gradetv.net/api/channels/:id/comments",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "DELETE",
      "path": "/api/comments/:id",
      "auth": "guest",
      "summary": "Apaga um comentário seu. Comentário alheio responde 404, não 403.",
      "grupo": "Comentários",
      "desc": "O 404 é de propósito: a API não confirma que existe um comentário com aquele id se ele não é seu.",
      "params": {
        "id": {
          "desc": "ID do comentário, vindo de `Comentario.id`.",
          "exemplo": "cm_9f3c2b1d7a4e58b0c2"
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        },
        "removed": {
          "tipo": "string",
          "desc": "O id que saiu."
        },
        "channel_id": {
          "tipo": "string",
          "desc": "Canal de onde o comentário saiu."
        }
      },
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/comments/CMT_ID -H \"X-Guest-Token: $IPT\"",
      "returns": "{ ok, removed, channel_id }",
      "url": "https://gradetv.net/api/comments/:id",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "GET",
      "path": "/api/chat/:channel_id/mensagens",
      "auth": "none",
      "summary": "Últimas mensagens da sala do canal, mais o endereço do WebSocket para acompanhar ao vivo.",
      "grupo": "Chat",
      "params": {
        "channel_id": {
          "desc": "ID do canal no catálogo.",
          "exemplo": "GloboNews.br"
        }
      },
      "retorno": {
        "channel_id": {
          "tipo": "string",
          "desc": "Canal a que a sala pertence."
        },
        "items": {
          "tipo": "MensagemChat[]",
          "desc": "As últimas mensagens, da mais antiga para a mais nova."
        },
        "watching": {
          "tipo": "int",
          "desc": "Quantas pessoas estão com a sala aberta agora."
        },
        "max_length": {
          "tipo": "int",
          "desc": "Tamanho máximo de uma mensagem."
        },
        "_links": {
          "tipo": "LinksChat",
          "desc": "Esta listagem, o WebSocket e a ficha do canal."
        }
      },
      "erros": {
        "404": "Canal não existe no catálogo."
      },
      "exemplo": "curl -s $ORIGIN/api/chat/GloboNews.br/mensagens",
      "returns": "{ channel_id, items[{id,autor,body,at}], watching, max_length, _links{self,websocket,channel} }",
      "url": "https://gradetv.net/api/chat/:channel_id/mensagens",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/chat/:channel_id/mensagens",
      "auth": "guest",
      "summary": "Manda mensagem na sala sem abrir WebSocket. Exige o passe mensal do chat.",
      "grupo": "Chat",
      "desc": "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.",
      "params": {
        "channel_id": {
          "desc": "ID do canal no catálogo.",
          "exemplo": "GloboNews.br"
        }
      },
      "corpo": {
        "body": {
          "tipo": "string",
          "desc": "O texto da mensagem, dentro de `max_length`.",
          "obrigatorio": true
        },
        "author": {
          "tipo": "string",
          "desc": "Apelido a usar; sem ele o servidor gera um estável."
        }
      },
      "body": {
        "body": "alguém aí?",
        "author": "Wendel"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true` quando a mensagem entrou."
        },
        "message": {
          "tipo": "MensagemChat",
          "desc": "A mensagem publicada na sala."
        }
      },
      "erros": {
        "400": "Texto vazio ou longo demais.",
        "401": null,
        "402": null,
        "404": "Canal não existe."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/chat/GloboNews.br/mensagens -H \"X-Guest-Token: $IPT\" -H \"X-PAYMENT: $PAGAMENTO\" -H 'content-type: application/json' -d '{\"body\":\"alguém aí?\"}'",
      "returns": "{ ok, message{id,autor,body,at} }",
      "url": "https://gradetv.net/api/chat/:channel_id/mensagens",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "POST",
      "path": "/api/chat/pass",
      "auth": "guest",
      "summary": "Compra ou confirma o passe mensal do chat: $0.10 por 30 dias, via x402 ou crédito.",
      "grupo": "Chat",
      "desc": "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`).",
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true` quando o passe está valendo ao fim da chamada."
        },
        "charged": {
          "tipo": "bool",
          "desc": "`true` se esta chamada cobrou; `false` se o passe já valia."
        },
        "until": {
          "tipo": "string",
          "desc": "Até quando o passe vale (UTC).",
          "nulo": true
        }
      },
      "erros": {
        "401": null,
        "402": null,
        "410": "Chave `iptk_…` (retirada): use o convidado.",
        "502": "`pago_nao_registrado`: pago e não gravado; traz o `recibo`. Repita com o mesmo `Idempotency-Key`, sem nova cobrança.",
        "503": "`pass_unavailable`: sem registro de compras, nada cobrado."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/chat/pass -H \"X-Guest-Token: $IPT\" -H \"X-PAYMENT: $PAGAMENTO\"",
      "returns": "{ ok, charged, until }",
      "url": "https://gradetv.net/api/chat/pass",
      "auth_detail": "Convidado `ipt_…` (`POST /api/guest`, em `X-Guest-Token` ou Bearer) ou, no navegador, a sessão da conta — com sessão, o dono é a conta, e escrever exige a mesma origem e o `X-CSRF-Token` de `/api/auth/bootstrap` (403 `invalid_origin`/`invalid_csrf`). Cookie da conta com a sessão vencida: 401 `session_ended`, nunca o convidado. Chave `iptk_…` retirada: 410 `api_key_retired`."
    },
    {
      "method": "GET",
      "path": "/api/chat/:channel_id/ws",
      "auth": "none",
      "summary": "WebSocket da sala do canal — o caminho ao vivo, com presença.",
      "grupo": "Chat",
      "desc": "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'}`.",
      "params": {
        "channel_id": {
          "desc": "ID do canal no catálogo.",
          "exemplo": "GloboNews.br"
        }
      },
      "retorno": {
        "_texto": "`101 Switching Protocols` e a conexão WebSocket; `426` sem o header de upgrade."
      },
      "erros": {
        "404": "Canal não existe no catálogo."
      },
      "returns": "`101 Switching Protocols` e a conexão WebSocket; `426` sem o header de upgrade.",
      "url": "https://gradetv.net/api/chat/:channel_id/ws",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/auth/bootstrap",
      "auth": "none",
      "grupo": "Conta",
      "summary": "Prepara o navegador para entrar na conta global.",
      "desc": "Define cookie HttpOnly restrito ao host. CSRF vinculado à sessão atual. Sem CORS.",
      "retorno": {
        "csrf": {
          "tipo": "string",
          "desc": "X-CSRF-Token"
        },
        "context": {
          "tipo": "string",
          "desc": "Opaque view context, also in X-MM-Context; not a credential / contexto opaco da vista, não é credencial."
        }
      },
      "erros": {
        "400": "invalid_request",
        "403": "invalid_origin / invalid_csrf",
        "503": "auth_unavailable: a sessão anterior é preservada / the previous session is preserved"
      },
      "returns": "{ csrf, context }",
      "url": "https://gradetv.net/api/auth/bootstrap",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/account/profile",
      "auth": "session",
      "grupo": "Conta",
      "exemplo": "await fetch(\"$ORIGIN/api/account/profile\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "summary": "Consulta seu perfil global.",
      "desc": "Lê preferências atuais da conta. Altere-as na página da conta; produtos não mantêm perfil autoritativo separado.",
      "retorno": {
        "_texto": "{profile:{name,locale,timeZone,theme,revision}}"
      },
      "erros": {
        "401": "invalid_session",
        "503": "auth_unavailable"
      },
      "returns": "{profile:{name,locale,timeZone,theme,revision}}",
      "url": "https://gradetv.net/api/account/profile",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "GET",
      "path": "/api/account/avatar",
      "auth": "session",
      "grupo": "Conta",
      "exemplo": "await fetch(\"$ORIGIN/api/account/avatar\", {credentials: \"same-origin\"}).then(r => {if (!r.ok) throw new Error(\"HTTP \" + r.status); return r.blob();});",
      "exemploLinguagem": "js",
      "summary": "Consulta sua foto de perfil global.",
      "desc": "WebP privado de até 64 KiB, sem cache. Altere-o na conta. Não aceita ID de usuário ou URL de objeto.",
      "retorno": {
        "_texto": "image/webp; Cache-Control: no-store"
      },
      "erros": {
        "401": "invalid_session",
        "404": "not_found: no photo / sem foto",
        "503": "auth_unavailable"
      },
      "returns": "image/webp; Cache-Control: no-store",
      "url": "https://gradetv.net/api/account/avatar",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/auth/logout",
      "auth": "session",
      "grupo": "Conta",
      "exemplo": "// Execute no console da página do produto / Run in the product page console.\n(async () => {\n  const origin = \"$ORIGIN\";\n  const {csrf} = await fetch(origin + \"/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(origin + \"/api/auth/logout\", {\n    method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({})\n  });\n  if (!r.ok) throw new Error(\"Auth HTTP \" + r.status);\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "summary": "Revoga esta sessão do produto.",
      "desc": "Exige bootstrap/CSRF deste navegador e sessão. As sessões de outros produtos permanecem ativas.",
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true"
        }
      },
      "erros": {
        "400": "invalid_request",
        "403": "invalid_origin / invalid_csrf",
        "503": "auth_unavailable: a sessão anterior é preservada / the previous session is preserved"
      },
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/auth/logout",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/auth/pwa/install",
      "auth": "session",
      "grupo": "App instalado",
      "summary": "Prepara uma instalação pessoal do app.",
      "desc": "Exige entrada recente, Origin exato e CSRF do navegador. Retorna PIN privado assinado e endereço do manifesto sem cache. Até 10 instalações ativas por conta e produto.",
      "corpo": {
        "path": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Same-origin task path, up to 1600 characters."
        },
        "state": {
          "tipo": "object",
          "obrigatorio": true,
          "desc": "Up to 30 allowlisted preference keys and string values."
        },
        "manifest": {
          "tipo": "object",
          "obrigatorio": true,
          "desc": "Public app name and same-origin icons."
        }
      },
      "retorno": {
        "_texto": "{pin,path,manifest}"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const manifest = await fetch(document.querySelector('link[rel=\"manifest\"]').href).then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/auth/pwa/install\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({path: location.pathname + location.search, state: {}, manifest})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "erros": {
        "400": "invalid_installation_path / invalid_installation_state / invalid_installation_manifest",
        "401": "invalid_session / reauth_required",
        "409": "installation_not_created",
        "503": "auth_unavailable"
      },
      "returns": "{pin,path,manifest}",
      "url": "https://gradetv.net/api/auth/pwa/install",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/auth/pwa/restore",
      "auth": "none",
      "grupo": "App instalado",
      "summary": "Restaura um app instalado.",
      "desc": "Exige PIN assinado, Origin exato e CSRF do navegador. Confere host, produto, conta, versão de segurança e revogação da instalação. Mantém a hora da autenticação original. Ações sensíveis continuam exigindo entrada recente.",
      "corpo": {
        "pin": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Private signed installation credential."
        }
      },
      "retorno": {
        "_texto": "{ok:true,state,path,manifest}; HttpOnly session cookies"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/auth/pwa/restore\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({pin: \"…\"})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "erros": {
        "401": "invalid_installation",
        "403": "invalid_origin / invalid_csrf",
        "503": "auth_unavailable"
      },
      "returns": "{ok:true,state,path,manifest}; HttpOnly session cookies",
      "url": "https://gradetv.net/api/auth/pwa/restore",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/auth/pwa/manifest",
      "auth": "none",
      "grupo": "App instalado",
      "summary": "Consulta o manifesto privado da instalação.",
      "desc": "Exige cookie da instalação e chave do manifesto. Privado e sem cache. A URL inicial guarda o PIN no fragmento.",
      "query": {
        "key": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Key in the manifest address that the install or restore call returned."
        }
      },
      "retorno": {
        "_texto": "application/manifest+json"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/auth/pwa/manifest?key=…\", {credentials: \"same-origin\"}).then(r => r.json())",
      "exemploLinguagem": "js",
      "erros": {
        "401": "invalid_installation"
      },
      "returns": "application/manifest+json",
      "url": "https://gradetv.net/api/auth/pwa/manifest",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/account/keys",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Lista suas chaves de API neste produto.",
      "desc": "Nunca devolve a chave: nome, 4 últimos caracteres, organização, criação, último uso (por hora) e se ainda vale.",
      "retorno": {
        "keys": {
          "tipo": "object[]",
          "desc": "`id`, `name`, `organizationId`, `last4`, `createdAt`, `lastUsedAt`, `revokedAt`, `active` (false quando revogada ou parada por troca de senha / encerrar todos os acessos)."
        }
      },
      "erros": {
        "401": "invalid_session",
        "503": "auth_unavailable"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/account/keys\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "returns": "{ keys }",
      "url": "https://gradetv.net/api/account/keys",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/account/keys/create",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Cria uma chave de API para agentes e scripts.",
      "desc": "Exige entrada feita há até 2 horas; 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.",
      "corpo": {
        "name": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Até 60 caracteres."
        },
        "organizationId": {
          "tipo": "string",
          "nulo": true,
          "obrigatorio": true,
          "desc": "`null` para chave da conta."
        }
      },
      "body": {
        "name": "agent",
        "organizationId": null
      },
      "retorno": {
        "key": {
          "tipo": "object",
          "desc": "`id`, `name`, `organizationId`, `last4`, `createdAt`."
        },
        "secret": {
          "tipo": "string",
          "desc": "`mmk_…`, mostrada uma vez."
        }
      },
      "erros": {
        "400": "invalid_key_name / invalid_organization",
        "401": "invalid_session / reauth_required",
        "403": "invalid_origin / invalid_csrf / organization_forbidden / organization_mfa_required",
        "409": "key_limit_reached",
        "503": "auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/account/keys/create\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({name: \"agent\", organizationId: null})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ key, secret }",
      "url": "https://gradetv.net/api/account/keys/create",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/account/keys/revoke",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Revoga uma das suas chaves de API.",
      "desc": "Para a chave na hora. Repetir não faz mal.",
      "corpo": {
        "id": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "O `id` da chave."
        }
      },
      "body": {
        "id": "…"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true"
        }
      },
      "erros": {
        "400": "invalid_key_id",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "404": "key_not_found",
        "503": "auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/account/keys/revoke\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({id: \"…\"})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/account/keys/revoke",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "GET",
      "path": "/api/account/apps",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Lista os apps conectados à sua conta neste produto.",
      "desc": "Os clientes de MCP em que você entrou com a conta (OAuth), como o claude.ai ou o ChatGPT. Nunca devolve token.",
      "retorno": {
        "apps": {
          "tipo": "object[]",
          "desc": "Até 50: `id`, `nome` (o app; o host dele quando ele se identifica por documento de metadados), `criadoEm`, `usadoEm` (por hora, ou null)."
        }
      },
      "erros": {
        "401": "invalid_session",
        "503": "auth_unavailable"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/account/apps\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "returns": "{ apps }",
      "url": "https://gradetv.net/api/account/apps",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/account/apps/revoke",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Desconecta um app da sua conta.",
      "desc": "Para o acesso do app na hora; ele pede o login de novo. Repetir não faz mal.",
      "corpo": {
        "id": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "O `id` do app."
        }
      },
      "body": {
        "id": "…"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true"
        }
      },
      "erros": {
        "400": "invalid_app_id",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "404": "app_not_found",
        "503": "auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/account/apps/revoke\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({id: \"…\"})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/account/apps/revoke",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "GET",
      "path": "/api/account/callouts",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Lista os avisos que você fechou neste produto.",
      "desc": "As chaves dos avisos que ficam fechados para a sua conta, em todo aparelho, como a explicação de uma área.",
      "retorno": {
        "callouts": {
          "tipo": "string[]",
          "desc": "Até 200 chaves, em ordem alfabética."
        }
      },
      "erros": {
        "401": "invalid_session",
        "403": "product_required",
        "503": "auth_unavailable"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/account/callouts\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "returns": "{ callouts }",
      "url": "https://gradetv.net/api/account/callouts",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/account/callouts/dismiss",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Fecha um aviso de vez.",
      "desc": "O aviso não abre de novo para a sua conta neste produto. Repetir não faz mal.",
      "corpo": {
        "key": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "A chave do aviso: letras, números e `_.:-`, até 80."
        }
      },
      "body": {
        "key": "financeiro_expense"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true"
        }
      },
      "erros": {
        "400": "invalid_callout",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "409": "too_many_callouts",
        "503": "auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/account/callouts/dismiss\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({key: \"financeiro_expense\"})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/account/callouts/dismiss",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/account/callouts/reopen",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Reabre um aviso fechado.",
      "desc": "Desfaz o fechar: o aviso volta para a sua conta neste produto. O produto guarda aqui também uma escolha de ligar e desligar, como os valores ocultos do Financeiro. Repetir não faz mal.",
      "corpo": {
        "key": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "A chave do aviso: letras, números e `_.:-`, até 80."
        }
      },
      "body": {
        "key": "financeiro_expense"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true"
        }
      },
      "erros": {
        "400": "invalid_callout",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "503": "auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/account/callouts/reopen\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({key: \"financeiro_expense\"})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/account/callouts/reopen",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/auth/wallet/agent/start",
      "auth": "none",
      "grupo": "Conta",
      "summary": "Começa a credencial de carteira do agente sem conta.",
      "desc": "Sem conta: a carteira prova que é dela assinando a mensagem (EIP-4361). Assinar não paga nem transfere nada. A mensagem diz este produto e vale 5 minutos. Antes de assinar, compare a mensagem com a URL chamada. O domínio da primeira linha deve ser o host dessa URL. O URI deve ser a origem dela. Se um deles for diferente, não assine. Assine com personal_sign e envie a /api/auth/wallet/agent/token.",
      "corpo": {
        "address": {
          "tipo": "string",
          "desc": "0x…, a carteira (EOA ou contrato na Base)",
          "obrigatorio": true
        }
      },
      "body": {
        "address": "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23"
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/auth/wallet/agent/start -H 'content-type: application/json' -d '{\"address\":\"0x2c7536E3605D9C16a7a3D7b1898e529396a65c23\"}'",
      "retorno": {
        "message": {
          "tipo": "string",
          "desc": "EIP-4361"
        },
        "issuedAt": {
          "tipo": "number",
          "desc": "segundos / seconds"
        },
        "expiresIn": {
          "tipo": "number",
          "desc": "300"
        }
      },
      "erros": {
        "400": "invalid_wallet_address",
        "503": "auth_unavailable"
      },
      "returns": "{ message, issuedAt, expiresIn }",
      "url": "https://gradetv.net/api/auth/wallet/agent/start",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/auth/wallet/agent/token",
      "auth": "none",
      "grupo": "Conta",
      "summary": "Recebe a credencial da carteira (mmw_…) deste produto.",
      "desc": "A assinatura da carteira na mensagem do start, conferida aqui (chave própria) ou na Base (contrato, EIP-1271/6492). A credencial vale 30 dias: envie `Authorization: Bearer mmw_…` às rotas que aceitam a carteira como dona (dono `x402:<endereço>`). Vinculada a carteira a uma conta, o que ela fez aqui passa para a conta quando a pessoa entra.",
      "corpo": {
        "address": {
          "tipo": "string",
          "desc": "0x…, a mesma carteira do start / the same wallet as in start",
          "obrigatorio": true
        },
        "issuedAt": {
          "tipo": "number",
          "desc": "o issuedAt do start / start's issuedAt",
          "obrigatorio": true
        },
        "signature": {
          "tipo": "string",
          "desc": "personal_sign (0x…)",
          "obrigatorio": true
        }
      },
      "body": {
        "address": "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23",
        "issuedAt": 1791400000,
        "signature": "0x…"
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/auth/wallet/agent/token -H 'content-type: application/json' -d '{\"address\":\"0x2c7536E3605D9C16a7a3D7b1898e529396a65c23\",\"issuedAt\":1791400000,\"signature\":\"0x…\"}'",
      "retorno": {
        "token": {
          "tipo": "string",
          "desc": "mmw_…"
        },
        "owner": {
          "tipo": "string",
          "desc": "x402:0x…"
        },
        "expiresAt": {
          "tipo": "number",
          "desc": "segundos / seconds"
        }
      },
      "erros": {
        "400": "invalid_body",
        "401": "invalid_wallet",
        "429": "wallet_limited",
        "503": "wallet_unavailable / auth_unavailable"
      },
      "returns": "{ token, owner, expiresAt }",
      "url": "https://gradetv.net/api/auth/wallet/agent/token",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/auth/claim/wallets",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Passa para a conta o que as carteiras dela fizeram sem conta.",
      "desc": "A janela de entrar chama depois de toda entrada. Para cada carteira vinculada à conta: os direitos comprados aqui por x402 (dono x402:<endereço>) e o que este produto guarda desse dono. Repetir não move nada.",
      "exemplo": "await MMConta.fetch(new URL(\"$ORIGIN/api/auth/claim/wallets\").pathname, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: \"{}\" })",
      "retorno": {
        "carteiras": {
          "tipo": "number",
          "desc": "wallets"
        },
        "direitos": {
          "tipo": "number",
          "desc": "rights moved"
        }
      },
      "erros": {
        "401": "invalid_session",
        "503": "auth_unavailable"
      },
      "returns": "{ carteiras, direitos }",
      "url": "https://gradetv.net/api/auth/claim/wallets",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/auth/claim",
      "auth": "session",
      "grupo": "Conta",
      "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.",
      "desc": "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).",
      "corpo": {
        "guest_token": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Convidado `ipt_…` deste navegador."
        }
      },
      "body": {
        "guest_token": "ipt_…"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Se o convidado foi reconhecido e passou."
        },
        "claimed": {
          "tipo": "object",
          "desc": "`product.movidos` (linhas que mudaram de dono, por tabela), `product.apagados` (duplicatas do convidado descartadas) e `product.direitos` (compras que passaram)."
        }
      },
      "erros": {
        "400": "invalid_product_claim / invalid_body",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "409": "unknown_guest (em `claimed.reason` / in `claimed.reason`)",
        "503": "product_claim_pending / auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/auth/claim\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({guest_token: localStorage.getItem(\"ipt_guest\")})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ ok, claimed }",
      "url": "https://gradetv.net/api/auth/claim",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "GET",
      "path": "/api/me",
      "auth": "session",
      "summary": "A conta da sessão: e-mail e o tamanho da biblioteca dela.",
      "desc": "A conta é a da biblioteca de conta; `user.id` é o id da conta, e é ele o dono da galeria com sessão.",
      "grupo": "Conta",
      "retorno": {
        "user": {
          "tipo": "Conta",
          "desc": "A pessoa dona da sessão."
        },
        "profile": {
          "tipo": "object",
          "desc": "Perfil global: `name`, `locale`, `timeZone`, `theme`, `revision`."
        },
        "app": {
          "tipo": "string",
          "desc": "Nome do produto."
        },
        "resources": {
          "tipo": "Recursos",
          "desc": "Quantas pastas, favoritos e canais no histórico a conta tem."
        }
      },
      "erros": {
        "401": "invalid_session",
        "503": "auth_unavailable"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/me\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "returns": "{ user{id,email}, profile, app, resources{categories,favorites,history} }",
      "url": "https://gradetv.net/api/me",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "GET",
      "path": "/api/billing",
      "auth": "none",
      "summary": "Preços em vigor, tetos da galeria e a configuração x402 completa.",
      "grupo": "Cobrança",
      "desc": "É 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.",
      "retorno": "Billing",
      "exemplo": "curl -s $ORIGIN/api/billing -H \"X-Guest-Token: $IPT\"",
      "returns": "{ provider, mode, network, chain_id, pay_to, homolog, dev, dev_gate, alternavel, gratis?, cobrados?, facilitator, asset, asset_address, faucet, wallets, product, quota{free,paid,how_to_pay,live,free_now?,charged_now?,trial?}, pricing, billing, api_index, prices{chat_month_usd,abuso_24h_usd}, chat{active,until}, limits{categories,groups_per_category,items_per_group} }",
      "url": "https://gradetv.net/api/billing",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/contact",
      "auth": "none",
      "summary": "Fale com quem faz o produto — de graça, para pessoa e agente.",
      "grupo": "Contato",
      "desc": "Uma rota para dúvida e para proposta de patrocínio, parceria ou anúncio (`tipo`, com os espaços de `GET /api/partners`). Sem captcha, sem conta, sem pagamento. Uma mensagem a cada 10 segundos por rede: a que chega antes espera a vez e sai — sem erro. A mensagem chega à equipe por e-mail, com o `email` como endereço de resposta. Para voz, envie multipart/form-data: `dados` contém este JSON (até 32 KiB) e `audio` contém um arquivo de áudio WebM, Ogg ou M4A (até 2 MB). O áudio pode substituir message. O navegador grava até dois minutos. O áudio vai como anexo ao e-mail do dono.",
      "corpo": {
        "name": {
          "tipo": "string",
          "desc": "Como chamar quem escreve (alias `nome`).",
          "obrigatorio": true
        },
        "email": {
          "tipo": "string",
          "desc": "Para onde responder.",
          "obrigatorio": true
        },
        "message": {
          "tipo": "string",
          "desc": "O que você quer dizer (alias `mensagem`).",
          "obrigatorio": true
        },
        "whatsapp": {
          "tipo": "string",
          "desc": "Número opcional para resposta, com código do país. Ex.: +5561999999999."
        },
        "prefer_whatsapp": {
          "tipo": "bool",
          "desc": "Pede resposta por WhatsApp. Exige whatsapp."
        },
        "tipo": {
          "tipo": "string",
          "desc": "Proposta: `patrocinio`, `parceria`, `anuncio` ou `consultoria`. Liga os campos abaixo."
        },
        "empresa": {
          "tipo": "string",
          "desc": "Quem propõe, quando é empresa."
        },
        "site": {
          "tipo": "string",
          "desc": "Site de quem propõe."
        },
        "orcamento": {
          "tipo": "string",
          "desc": "Anúncio: `ate_100`, `100_500`, `500_2000`, `2000_mais` ou `a_combinar`. Consultoria, em dólar: `ate_2000`, `2000_5000`, `5000_10000`, `10000_mais` ou `nao_sei`."
        },
        "espaco": {
          "tipo": "string[]",
          "desc": "Ids de placement de `GET /api/partners`, até 6."
        },
        "duracao": {
          "tipo": "string",
          "desc": "Dias de exposição: `30`, `90` ou `365`."
        },
        "pagamento": {
          "tipo": "string",
          "desc": "`usdc`, `deposito` ou `a_combinar`."
        },
        "projeto": {
          "tipo": "string",
          "desc": "Consultoria: o tipo de projeto (`llm_privado`, `voz`, `dados_sensiveis`, `gpu`, `integracao` ou `outro`)."
        },
        "prazo": {
          "tipo": "string",
          "desc": "Consultoria: quando começar (`agora`, `1_mes`, `1_3_meses` ou `explorando`)."
        },
        "formato": {
          "tipo": "string",
          "desc": "Consultoria: `projeto` (por escopo), `mensal` ou `assessoria`."
        },
        "pais": {
          "tipo": "string",
          "desc": "Consultoria: o país de quem pede."
        },
        "fuso": {
          "tipo": "string",
          "desc": "Consultoria: o fuso IANA, como `America/New_York`."
        },
        "horarios": {
          "tipo": "string[]",
          "desc": "Consultoria: até 3 horários para a conversa, `AAAA-MM-DDTHH:MM` na hora do fuso."
        }
      },
      "body": {
        "name": "Agente",
        "email": "agent@example.com",
        "message": "olá, sou um agente"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true` quando a mensagem foi aceita."
        }
      },
      "erros": {
        "400": "Validação: o `code` diz o campo.",
        "503": "O contato não está configurado neste servidor."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/contact -H 'content-type: application/json' -d '{\"name\":\"Agente\",\"email\":\"agent@example.com\",\"message\":\"olá, sou um agente\"}'",
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/contact",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/visit",
      "auth": "none",
      "summary": "O aviso da tela que soma uma visita ao dia. Agentes não precisam chamar.",
      "grupo": "Operação",
      "desc": "A tela manda o aviso no primeiro gesto de quem usa (toque, tecla ou clique), uma vez por página. Teste não conta: `X-MM-Smoke`, User-Agent `mm-smoke` ou `smoke: true` no corpo voltam com `counted: false`. Só o aviso da própria página conta: a chamada de fora de uma página do mesmo site (User-Agent de robô, ou sem `Sec-Fetch-Site: same-origin`) volta com `counted: false` e `reason: \"robo\"`.",
      "corpo": {
        "p": {
          "tipo": "string",
          "desc": "O caminho da página visitada."
        },
        "smoke": {
          "tipo": "bool",
          "desc": "`true` marca o pedido como teste, e ele não entra na conta."
        }
      },
      "body": {
        "p": "/"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        },
        "counted": {
          "tipo": "bool",
          "desc": "Se a visita entrou na conta do dia."
        },
        "reason": {
          "tipo": "string",
          "desc": "Por que não contou, quando `counted` é `false`.",
          "opcional": true
        }
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/visit -H 'content-type: application/json' -d '{\"p\":\"/\",\"smoke\":true}'",
      "returns": "{ ok, counted, reason? }",
      "url": "https://gradetv.net/api/visit",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/erro-cliente",
      "auth": "none",
      "summary": "Relato de erro do navegador, enviado pela própria interface. Agente não precisa chamar.",
      "grupo": "Operação",
      "desc": "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.",
      "corpo": {
        "code": {
          "tipo": "string",
          "desc": "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).",
          "obrigatorio": true
        },
        "phase": {
          "tipo": "string",
          "desc": "Fase em que quebrou, minúsculas: `global`, `promessa`, `script`, `carregar_lista`…",
          "obrigatorio": true
        },
        "path": {
          "tipo": "string",
          "desc": "Caminho da página aberta, sem query; número, hash e token no caminho são guardados como `:id`."
        },
        "message": {
          "tipo": "string",
          "desc": "Mensagem do erro, até 2000 caracteres."
        },
        "stack": {
          "tipo": "string",
          "desc": "Stack trace, até 12000 caracteres."
        },
        "source": {
          "tipo": "string",
          "desc": "Script de origem; só o caminho é guardado."
        },
        "line": {
          "tipo": "int",
          "desc": "Linha no script de origem."
        },
        "column": {
          "tipo": "int",
          "desc": "Coluna no script de origem."
        },
        "visivel": {
          "tipo": "bool",
          "desc": "Se a aba estava visível quando quebrou."
        },
        "build": {
          "tipo": "string",
          "desc": "Build da página que relatou (a `<meta name=\"build\">`), até 64 letras, dígitos, `.`, `_` ou `-`; é ele que data a falha."
        }
      },
      "body": {
        "code": "UI-APP-001",
        "phase": "carregar_lista",
        "path": "/",
        "message": "lista 500"
      },
      "retorno": {
        "_texto": "204 sem corpo, sempre — relato inválido, repetido ou acima do teto também recebe 204."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/erro-cliente -H 'content-type: application/json' -d '{\"code\":\"UI-APP-001\",\"phase\":\"carregar_lista\",\"path\":\"/\",\"message\":\"lista 500\"}'",
      "returns": "204 sem corpo, sempre — relato inválido, repetido ou acima do teto também recebe 204.",
      "url": "https://gradetv.net/api/erro-cliente",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/pagamento/aberto",
      "auth": "none",
      "statusOk": 202,
      "summary": "A interface relata que exibiu uma cobrança. Agentes não devem chamar.",
      "grupo": "Operação",
      "desc": "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.",
      "headers": {
        "Origin": {
          "tipo": "string",
          "desc": "A origem da página, idêntica à desta rota.",
          "obrigatorio": true
        },
        "Sec-Fetch-Site": {
          "tipo": "string",
          "desc": "`same-origin`, definido pelo navegador.",
          "obrigatorio": true
        },
        "X-MM-Payment-View": {
          "tipo": "string",
          "desc": "`1`, definido pelo componente comum.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "_texto": "202 sem corpo se aceito; 204 se ignorado. Sempre no-store."
      },
      "returns": "202 sem corpo se aceito; 204 se ignorado. Sempre no-store.",
      "url": "https://gradetv.net/api/pagamento/aberto",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/funil",
      "auth": "none",
      "statusOk": 202,
      "summary": "A interface relata os passos da visita (funil de conversão). Agentes não devem chamar.",
      "grupo": "Operação",
      "desc": "Lote da mesma origem, enviado pela própria página: páginas vistas, engajamento, oferta à vista, clique em comprar, janela de pagamento, pagamento enviado ou aceito. Guarda o id aleatório do navegador, o caminho sem query, o host de quem mandou a pessoa e as utm; nunca IP, e-mail ou conta. Não grava banco: uma linha por lote no diário do dia, com teto. Robô declarado e smoke ficam de fora.",
      "headers": {
        "Origin": {
          "tipo": "string",
          "desc": "A origem da página, idêntica à desta rota.",
          "obrigatorio": false
        },
        "Content-Type": {
          "tipo": "string",
          "desc": "`application/json` ou `text/plain`.",
          "obrigatorio": true
        }
      },
      "corpo": {
        "v": {
          "tipo": "number",
          "desc": "Versão do lote: `1`.",
          "obrigatorio": true
        },
        "vid": {
          "tipo": "string",
          "desc": "Id aleatório deste navegador (UUID v4).",
          "obrigatorio": true
        },
        "sid": {
          "tipo": "string",
          "desc": "Id da sessão (30 min sem atividade encerram).",
          "obrigatorio": true
        },
        "sn": {
          "tipo": "number",
          "desc": "Número da sessão deste navegador.",
          "obrigatorio": true
        },
        "pv": {
          "tipo": "string",
          "desc": "Id da página vista.",
          "obrigatorio": true
        },
        "e": {
          "tipo": "object[]",
          "desc": "Até 40 eventos `{ t, n, p? }` do vocabulário do funil.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "_texto": "202 sem corpo se guardado; 204 se ignorado. Sempre no-store."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/funil -H 'content-type: text/plain' -H 'x-mm-smoke: 1' -d '{\"v\":1,\"vid\":\"0f8b9c1e-2a3b-4c5d-8e6f-7a8b9c0d1e2f\",\"sid\":\"s1abcdefgh\",\"sn\":1,\"pv\":\"p1abcdefgh\",\"e\":[{\"t\":0,\"n\":\"pagina\"}]}'",
      "returns": "202 sem corpo se guardado; 204 se ignorado. Sempre no-store.",
      "url": "https://gradetv.net/api/funil",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/funil/arquivos",
      "auth": "none",
      "summary": "Operador: os dias do diário do funil guardados neste app, com o tamanho de cada um.",
      "grupo": "Operação",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>`.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "v": {
          "tipo": "number",
          "desc": "Versão do lote guardado."
        },
        "produto": {
          "tipo": "string",
          "desc": "O produto."
        },
        "teto": {
          "tipo": "object",
          "desc": "`bytesDia` e `dias` guardados."
        },
        "dias": {
          "tipo": "object[]",
          "desc": "`{ dia, bytes }`, do mais velho ao de hoje."
        }
      },
      "erros": {
        "401": "Unauthorized",
        "503": "Not configured"
      },
      "exemplo": "curl -s $ORIGIN/api/funil/arquivos -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "{ v, produto, teto, dias }",
      "url": "https://gradetv.net/api/funil/arquivos",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/funil/arquivo",
      "auth": "none",
      "summary": "Operador: um pedaço de um dia do diário do funil, em NDJSON, a partir de um byte.",
      "grupo": "Operação",
      "desc": "Até 4 MiB por resposta, cortados na última linha inteira. `X-MM-Funil-Proximo` diz de onde pedir o resto; `X-MM-Funil-Tamanho`, o tamanho do dia agora.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>`.",
          "obrigatorio": true
        }
      },
      "query": {
        "dia": {
          "tipo": "string",
          "desc": "O dia, `AAAA-MM-DD` (UTC).",
          "obrigatorio": true
        },
        "desde": {
          "tipo": "number",
          "desc": "O byte de onde ler; `0` no começo.",
          "obrigatorio": false
        }
      },
      "retorno": {
        "_texto": "Linhas JSON, uma por lote guardado."
      },
      "erros": {
        "400": "Invalid parameters",
        "401": "Unauthorized",
        "503": "Not configured"
      },
      "exemplo": "curl -s \"$ORIGIN/api/funil/arquivo?dia=2026-09-27&desde=0\" -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "Linhas JSON, uma por lote guardado.",
      "url": "https://gradetv.net/api/funil/arquivo",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/vitrine",
      "auth": "none",
      "grupo": "Números públicos",
      "summary": "Os números públicos do produto: tráfego, agentes, uso e confiabilidade, sem dinheiro.",
      "desc": "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.",
      "retorno": {
        "v": {
          "tipo": "int",
          "desc": "Versão do contrato (1)."
        },
        "produto": {
          "tipo": "string",
          "desc": "Id do produto."
        },
        "publicado": {
          "tipo": "bool",
          "desc": "`false` antes da primeira publicação do coletor; aí só estas cinco chaves vêm."
        },
        "atualizado_em": {
          "tipo": "string",
          "desc": "Quando o coletor publicou (ISO 8601).",
          "nulo": true
        },
        "stale": {
          "tipo": "bool",
          "desc": "`true` quando a projeção tem mais de 26 h."
        },
        "nome": {
          "tipo": "string",
          "desc": "Nome do produto.",
          "opcional": true
        },
        "desde": {
          "tipo": "string",
          "desc": "Dia a partir do qual a série vale.",
          "nulo": true,
          "opcional": true
        },
        "fuso": {
          "tipo": "string",
          "desc": "Fuso dos dias de tráfego. A audiência informa seu próprio fuso em `pessoas.medicao`.",
          "opcional": true
        },
        "hoje": {
          "tipo": "object",
          "desc": "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.",
          "opcional": true
        },
        "dias": {
          "tipo": "object[]",
          "desc": "Até 31 dias, o mais antigo primeiro: `dia`, `paginas`, `api`, `api_ia`, `maquina`, `visitantes`, `uso`.",
          "opcional": true
        },
        "janelas": {
          "tipo": "object",
          "desc": "Somas de 7 e 30 dias (`d7`, `d30`).",
          "opcional": true
        },
        "visitantes": {
          "tipo": "object",
          "desc": "Navegadores únicos com interação registrada, em 7 dias.",
          "opcional": true
        },
        "pessoas": {
          "tipo": "object",
          "desc": "Medição própria de navegadores confirmados por interação: únicos, sessões e páginas em 7 e 30 dias, recortes e cobertura. `null` indica ausência de medição. Não identifica pessoas verificadas.",
          "nulo": true,
          "opcional": true
        },
        "agentes": {
          "tipo": "object",
          "desc": "Os agentes de IA e os bots que mais leem, 7 dias.",
          "opcional": true
        },
        "superficies": {
          "tipo": "object",
          "desc": "Leituras de OKF, llms, well-known, OpenAPI e MCP em 7 dias.",
          "opcional": true
        },
        "mcp": {
          "tipo": "object",
          "desc": "Chamadas MCP em 7 dias.",
          "opcional": true
        },
        "uso": {
          "tipo": "object",
          "desc": "Uso real do produto por recurso: rótulo, hoje, 7 e 30 dias.",
          "opcional": true
        },
        "contas": {
          "tipo": "object",
          "desc": "Usuários e convidados.",
          "nulo": true,
          "opcional": true
        },
        "confiabilidade": {
          "tipo": "object",
          "desc": "Percentual de pedidos sem 5xx em 7 dias e o build no ar.",
          "opcional": true
        },
        "catalogo": {
          "tipo": "object",
          "desc": "Tamanho do acervo, quando o produto tem um.",
          "nulo": true,
          "opcional": true
        },
        "apoio": {
          "tipo": "object",
          "desc": "Apoios com medição: exposições por 1 s e cliques nativos por página em sessões confirmadas, CTR de 7 dias, dias observados e cobertura. Vazio é ausência de histórico, não CTR zero.",
          "opcional": true
        }
      },
      "exemplo": "curl -s $ORIGIN/api/vitrine",
      "returns": "{ v, produto, publicado, atualizado_em, stale, nome?, desde?, fuso?, hoje?, dias?, janelas?, visitantes?, pessoas?, agentes?, superficies?, mcp?, uso?, contas?, confiabilidade?, catalogo?, apoio? }",
      "url": "https://gradetv.net/api/vitrine",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/vitrine/operador",
      "auth": "none",
      "grupo": "Números públicos",
      "summary": "O documento completo do produto no painel do operador — só com o token do operador.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` — a classe operador.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "produto": {
          "tipo": "string",
          "desc": "Id do produto."
        },
        "atualizado_em": {
          "tipo": "string",
          "desc": "Quando o coletor publicou.",
          "nulo": true
        },
        "operador": {
          "tipo": "object",
          "desc": "O documento completo do coletor, com o que a projeção pública não carrega.",
          "nulo": true
        }
      },
      "erros": {
        "401": "Sem token, token errado ou token de outra classe.",
        "503": "Worker sem `METRICS_TOKEN` ou sem o control plane."
      },
      "exemplo": "curl -s $ORIGIN/api/vitrine/operador -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "{ produto, atualizado_em, operador }",
      "url": "https://gradetv.net/api/vitrine/operador",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/vitrine/painel",
      "auth": "none",
      "grupo": "Números públicos",
      "summary": "O painel da casa inteira, na forma que o gm lê — só com o token do operador.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` — a classe operador.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "apps": {
          "tipo": "object[]",
          "desc": "Um documento do operador por produto, em ordem de id."
        },
        "updated": {
          "tipo": "string",
          "desc": "Quando o coletor fechou a rodada.",
          "opcional": true
        },
        "totals": {
          "tipo": "object",
          "desc": "Os totais da casa.",
          "opcional": true
        }
      },
      "erros": {
        "401": "Sem token, token errado ou token de outra classe.",
        "503": "Worker sem `METRICS_TOKEN` ou sem o control plane."
      },
      "exemplo": "curl -s $ORIGIN/api/vitrine/painel -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "{ apps, updated?, totals? }",
      "url": "https://gradetv.net/api/vitrine/painel",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/vitrine/cursores",
      "auth": "none",
      "grupo": "Números públicos",
      "summary": "Cursores por produto: `borda`, `borda5xx` (só 5xx) e `cli`. Só com token do operador.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` — a classe operador.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "_texto": "JSON: `{ [produto]: { borda?: ISO, borda5xx?: ISO, cli?: ISO } }`; vazio é `{}`."
      },
      "erros": {
        "401": "Sem token, token errado ou token de outra classe.",
        "503": "Worker sem `METRICS_TOKEN` ou sem o control plane."
      },
      "exemplo": "curl -s $ORIGIN/api/vitrine/cursores -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "JSON: `{ [produto]: { borda?: ISO, borda5xx?: ISO, cli?: ISO } }`; vazio é `{}`.",
      "url": "https://gradetv.net/api/vitrine/cursores",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/partners",
      "auth": "none",
      "grupo": "Parceria",
      "summary": "Anúncios com preço fechado: espaços, duração, público e contratação. Parcerias e campanhas diferentes seguem por negociação.",
      "desc": "Quatro pacotes com preço em USD por semana, contratação avulsa ou assinatura semanal no cartão. Inclui canais reais, patrocinadores e números públicos. Contratação pelo POST /api/partners. Negociações e parcerias usam o contato. Cache de até 1 hora; com pedido vinculado, 1 minuto.",
      "retorno": {
        "status": {
          "tipo": "string",
          "desc": "`preco_fechado`: os espaços com preço podem ser contratados; os demais são negociados."
        },
        "produto": {
          "tipo": "string",
          "desc": "Nome do produto."
        },
        "idioma": {
          "tipo": "string",
          "desc": "Idioma dos textos (o do produto)."
        },
        "titulo": {
          "tipo": "string",
          "desc": "Título da oferta."
        },
        "descricao": {
          "tipo": "string",
          "desc": "Uma frase sobre a oferta."
        },
        "publico": {
          "tipo": "string",
          "desc": "Quem usa o produto — o público que o patrocinador alcança."
        },
        "modalidades": {
          "tipo": "object[]",
          "desc": "`{ id, nome }`: patrocinio, parceria, anuncio."
        },
        "placements": {
          "tipo": "object[]",
          "desc": "Canais incluídos nos pacotes: id, nome, onde, formato, medicao e disponivel. Preço mensal legado nulo no catálogo semanal."
        },
        "packages": {
          "tipo": "object[]",
          "desc": "Quatro pacotes: id, nome, assinatura, preco_usd_semana, espacos, canais e períodos permitidos em exposicao[{dias,preco}]."
        },
        "house_bundle": {
          "tipo": "object",
          "desc": "Indisponível no catálogo semanal: `{disponivel:false}`."
        },
        "parcerias": {
          "tipo": "string[]",
          "desc": "Ideias de parceria que o produto aceita discutir."
        },
        "current_sponsors": {
          "tipo": "object[]",
          "desc": "Patrocinadores em vigor: `id`, `nome`, `url`, `frase`, `espacos`, `ate`."
        },
        "stats": {
          "tipo": "object",
          "desc": "Recorte dos números públicos (`hoje`, `janelas`, `agentes`, `confiabilidade`) e o `link` para `/api/vitrine`; `publicado: false` antes da primeira publicação."
        },
        "payment": {
          "tipo": "object",
          "desc": "Pagamento compartilhado pelo POST da contratação. Os campos de carteira permanecem para compatibilidade; não envie pagamento manual antes da cotação."
        },
        "contact": {
          "tipo": "object",
          "desc": "`email`, `form_url`, `api_url` (`POST /api/contact`, livre: uma mensagem a cada 10 s por rede), `campos` (os obrigatórios), `campos_proposta` (os opcionais da proposta, com os valores aceitos de cada um), `message_template`, `instructions`."
        },
        "politica": {
          "tipo": "object",
          "desc": "Rótulo do espaço, setores recusados, pagamento adiantado, prazos."
        },
        "_links": {
          "tipo": "object",
          "desc": "`self`, `stats`, `page` (`null` até a página existir), `contact`, `casa` (o mesmo caminho nos dez produtos)."
        }
      },
      "exemplo": "curl -s $ORIGIN/api/partners",
      "returns": "{ status, produto, idioma, titulo, descricao, publico, modalidades, placements, packages, house_bundle, parcerias, current_sponsors, stats, payment, contact, politica, _links }",
      "url": "https://gradetv.net/api/partners",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/partners",
      "auth": "none",
      "grupo": "Parceria",
      "summary": "Contrate um pacote semanal de anúncios e envie a arte para revisão.",
      "desc": "O servidor calcula o preço do pacote e das semanas. Avulso: cotação 402 e pagamento compartilhado, sem renovação. modalidade=assinatura: conta autenticada, CSRF da mesma origem, cartão nativo e aceite explícito da renovação semanal. Consulte a oferta atual em BRL em /api/credito/assinaturas/ofertas. Cancele as próximas cobranças na conta. modalidade=recibo consulta apenas o recibo da campanha da própria conta. A cobrança precede a revisão; a exibição começa na publicação. Repetições mantêm id, token e conteúdo. A cotação não grava arte nem reserva espaço. Campanhas diferentes e parcerias gratuitas usam POST /api/contact.",
      "query": [
        {
          "nome": "modalidade",
          "tipo": "string",
          "desc": "Omit for one-off payment; assinatura uses native weekly card billing; recibo reads the account's campaign receipt."
        }
      ],
      "corpo": {
        "id": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Fresh random order id: 32 lowercase hexadecimal characters. Reuse for retries."
        },
        "token": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Private random order capability: 64 lowercase hexadecimal characters. Keep it for recovery."
        },
        "espaco": {
          "tipo": "string[]",
          "desc": "Legacy paid-order retry only. New purchases select a package."
        },
        "pacote": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "One of the four package ids in GET /api/partners packages."
        },
        "duracao": {
          "tipo": "int",
          "obrigatorio": true,
          "desc": "Days from the package exposure list. Weekly subscriptions require 7."
        },
        "assinatura": {
          "tipo": "object",
          "desc": "Only with modalidade=assinatura: the native subscription body with recurso=publicidade:ORDER_ID, simulado, cartao and explicit renewal aceite. The authenticated account must own the campaign."
        },
        "pedido": {
          "tipo": "string",
          "desc": "Only with modalidade=recibo: the campaign id owned by the signed-in account."
        },
        "nome": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Brand or product, up to 80 characters."
        },
        "email": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Contact email."
        },
        "url": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "HTTPS destination URL."
        },
        "frase": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Ad copy, up to 100 characters."
        },
        "arte": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Base64 image bytes, without a data URL prefix. PNG, JPEG or WebP, up to 2 MB."
        },
        "direitos": {
          "tipo": "bool",
          "obrigatorio": true,
          "desc": "true confirms the right to use the image and content."
        }
      },
      "retorno": {
        "id": {
          "tipo": "string",
          "desc": "Order id."
        },
        "estado": {
          "tipo": "string",
          "desc": "revisao: paid and awaiting content approval."
        },
        "recibo": {
          "tipo": "string",
          "desc": "Verified payment receipt."
        }
      },
      "erros": {
        "400": "Invalid order or artwork.",
        "402": "Payment required.",
        "409": "Space occupied or order changed.",
        "503": "Checkout unavailable."
      },
      "exemplo": "curl -X POST '$ORIGIN/api/partners' -H 'content-type: application/json' -H 'Idempotency-Key: ORDER_ID' --data-binary @campaign.json",
      "returns": "{ id, estado, recibo }",
      "url": "https://gradetv.net/api/partners",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/newsletter",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Consulte a autorização de publicidade por e-mail.",
      "desc": "Somente a conta da sessão. Não recebe identificador de outra conta. Resposta sem cache.",
      "retorno": {
        "ativo": {
          "tipo": "bool",
          "desc": "Autorização separada para publicidade."
        },
        "areas": {
          "tipo": "string[]",
          "desc": "[\"publicidade\"]."
        },
        "idioma": {
          "tipo": "string",
          "desc": "pt | en."
        },
        "verificado": {
          "tipo": "bool",
          "desc": "A conta tem e-mail confirmado."
        }
      },
      "erros": {
        "401": "invalid_session",
        "503": "newsletter_indisponivel"
      },
      "exemplo": "curl -s \"$ORIGIN/api/newsletter\" -b \"$COOKIE\"",
      "returns": "{ ativo, areas, idioma, verificado }",
      "url": "https://gradetv.net/api/newsletter",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "PUT",
      "path": "/api/newsletter",
      "auth": "session",
      "grupo": "Conta",
      "summary": "Autorize ou pare publicidade nos avisos solicitados.",
      "desc": "Exige sessão, origem e X-CSRF-Token. Ativar exige consentimento explícito e e-mail confirmado. Retirar é imediato e preserva avisos essenciais. Até uma publicidade por conta a cada 24 horas.",
      "headers": {
        "X-CSRF-Token": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Valor CSRF fornecido pelo SDK."
        }
      },
      "corpo": {
        "ativo": {
          "tipo": "bool",
          "obrigatorio": true,
          "desc": "true autoriza; false retira."
        },
        "areas": {
          "tipo": "string[]",
          "obrigatorio": true,
          "desc": "[\"publicidade\"]."
        },
        "idioma": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Idioma dos avisos: pt ou en."
        },
        "consentimento": {
          "tipo": "bool",
          "desc": "true ao autorizar."
        }
      },
      "body": {
        "ativo": false,
        "areas": [
          "publicidade"
        ],
        "idioma": "en"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true."
        },
        "ativo": {
          "tipo": "bool",
          "desc": "Autorização separada para publicidade."
        },
        "areas": {
          "tipo": "string[]",
          "desc": "[\"publicidade\"]."
        },
        "idioma": {
          "tipo": "string",
          "desc": "pt | en."
        }
      },
      "erros": {
        "400": "assinatura_invalida",
        "401": "invalid_session",
        "403": "invalid_csrf | email_verificado_exigido",
        "429": "aguarde_para_alterar | limite_diario",
        "503": "newsletter_indisponivel"
      },
      "exemplo": "curl -X PUT \"$ORIGIN/api/newsletter\" -b \"$COOKIE\" -H \"Origin: $ORIGIN\" -H \"X-CSRF-Token: $CSRF\" -H \"Content-Type: application/json\" -d '{\"ativo\":false,\"areas\":[\"publicidade\"],\"idioma\":\"en\"}'",
      "returns": "{ ok, ativo, areas, idioma }",
      "url": "https://gradetv.net/api/newsletter",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/newsletter/cancelar",
      "auth": "none",
      "grupo": "Conta",
      "summary": "Retire a autorização pelo link recebido no e-mail.",
      "desc": "O token secreto do link substitui a sessão. JSON exige a origem do produto. Também aceita RFC 8058: corpo List-Unsubscribe=One-Click e token na query, sem login. Avisos essenciais continuam.",
      "corpo": {
        "token": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Token de 43 caracteres do link, sem o prefixo."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true."
        }
      },
      "erros": {
        "400": "cancelamento_invalido | confirmacao_exigida",
        "403": "invalid_origin",
        "404": "cancelamento_invalido_ou_vencido"
      },
      "exemplo": "curl \"$ORIGIN/api/newsletter/cancelar\" -H \"Origin: $ORIGIN\" -H \"Content-Type: application/json\" -d '{\"token\":\"<link-token>\"}'",
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/newsletter/cancelar",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/metrics",
      "auth": "none",
      "summary": "Métricas dos últimos 7 dias. Com o token do operador, inclui os pagamentos.",
      "grupo": "Cobrança",
      "desc": "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 — e `compras`, quem pagou nos últimos 30 dias.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` para incluir o bloco financeiro.",
          "obrigatorio": false
        }
      },
      "retorno": "Metricas",
      "exemplo": "curl -s $ORIGIN/api/metrics -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "{ app, today, today_visits, today_contacts?, today_plays, today_feeds, days, usage, accounts, top_plays, financeiro?, payments? }",
      "url": "https://gradetv.net/api/metrics",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/avisos",
      "auth": "session",
      "grupo": "Avisos",
      "summary": "Os avisos agendados e as regras de avisar sempre da conta num canal.",
      "desc": "Só com conta: o convidado não tem e-mail. `chave` é a chave pública VAPID do servidor para o navegador assinar a notificação; `null` quando a notificação do navegador está desligada.",
      "query": {
        "canal": {
          "tipo": "string",
          "desc": "Slug do canal, o da página da programação.",
          "exemplo": "tv-brasil",
          "obrigatorio": true
        }
      },
      "retorno": "AvisosDaConta",
      "erros": {
        "400": "canal_invalido",
        "401": "invalid_session",
        "404": "canal_nao_encontrado"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/avisos?canal=tv-brasil\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "returns": "{ avisos[{id,inicio,titulo,antes,meios,regra}], regras[{id,chave,titulo,antes,meios}], chave }",
      "url": "https://gradetv.net/api/avisos",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/avisos",
      "auth": "session",
      "grupo": "Avisos",
      "summary": "Marca o aviso antes de um programa da guia e, com `sempre`, antes de cada exibição dele no canal.",
      "desc": "O programa precisa estar na guia do canal com o mesmo começo e o mesmo título. Marcar de novo o mesmo programa troca a escolha. Até 100 avisos agendados e 30 regras por conta.",
      "corpo": {
        "canal": {
          "tipo": "string",
          "desc": "Slug do canal, o da página da programação.",
          "exemplo": "tv-brasil",
          "obrigatorio": true
        },
        "inicio": {
          "tipo": "string",
          "desc": "Começo do programa, ISO 8601, como a guia dá.",
          "obrigatorio": true
        },
        "titulo": {
          "tipo": "string",
          "desc": "Título do programa, como a guia dá; até 300 caracteres.",
          "obrigatorio": true
        },
        "antes": {
          "tipo": "int",
          "desc": "Minutos antes do começo: 0, 5, 10, 15, 30 ou 60.",
          "obrigatorio": true
        },
        "meios": {
          "tipo": "string[]",
          "desc": "`email` e, no navegador que guardou a assinatura, `navegador`.",
          "obrigatorio": true
        },
        "sempre": {
          "tipo": "bool",
          "desc": "Avisar antes de toda exibição deste título no canal."
        }
      },
      "retorno": "AvisosDaConta",
      "erros": {
        "400": "aviso_invalido",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "404": "canal_nao_encontrado ou programa_nao_encontrado",
        "409": "comecou: o programa já começou",
        "429": "teto: avisos ou regras demais na conta"
      },
      "exemplo": "curl -X POST \"$ORIGIN/api/avisos\" -b cookies.txt -H \"Origin: $ORIGIN\" -H \"X-CSRF-Token: SESSION_CSRF\" -H \"Content-Type: application/json\" -d '{\"canal\":\"tv-brasil\",\"inicio\":\"2026-11-02T13:00:00.000Z\",\"titulo\":\"Mundo Bita\",\"antes\":10,\"meios\":[\"email\"]}'",
      "returns": "{ avisos[{id,inicio,titulo,antes,meios,regra}], regras[{id,chave,titulo,antes,meios}], chave }",
      "url": "https://gradetv.net/api/avisos",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "DELETE",
      "path": "/api/avisos/:id",
      "auth": "session",
      "grupo": "Avisos",
      "summary": "Cancela um aviso agendado da conta.",
      "params": {
        "id": {
          "desc": "ID do aviso, o de `avisos[].id`.",
          "exemplo": "42"
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        }
      },
      "erros": {
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "404": "not_found"
      },
      "exemplo": "curl -X DELETE \"$ORIGIN/api/avisos/42\" -b cookies.txt -H \"Origin: $ORIGIN\" -H \"X-CSRF-Token: SESSION_CSRF\"",
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/avisos/:id",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "DELETE",
      "path": "/api/avisos/regras/:id",
      "auth": "session",
      "grupo": "Avisos",
      "summary": "Para de avisar sempre e cancela os avisos agendados que a regra marcou.",
      "params": {
        "id": {
          "desc": "ID da regra, o de `regras[].id`.",
          "exemplo": "7"
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        }
      },
      "erros": {
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "404": "not_found"
      },
      "exemplo": "curl -X DELETE \"$ORIGIN/api/avisos/regras/7\" -b cookies.txt -H \"Origin: $ORIGIN\" -H \"X-CSRF-Token: SESSION_CSRF\"",
      "returns": "{ ok }",
      "url": "https://gradetv.net/api/avisos/regras/:id",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "POST",
      "path": "/api/avisos/navegador",
      "auth": "session",
      "grupo": "Avisos",
      "summary": "Guarda a assinatura da notificação deste navegador e, quando ela é nova, manda uma confirmação.",
      "desc": "A assinatura é a da Push API (`PushSubscription.toJSON()`), feita com a `chave` de `GET /api/avisos`. Só os serviços de push dos navegadores (Google, Mozilla, Apple e Microsoft) são aceitos. Até 10 navegadores por conta.",
      "corpo": {
        "assinatura": {
          "tipo": "object",
          "desc": "`{endpoint, keys: {p256dh, auth}}` da Push API.",
          "obrigatorio": true
        },
        "canal": {
          "tipo": "string",
          "desc": "Slug do canal que a confirmação abre.",
          "exemplo": "tv-brasil"
        },
        "lingua": {
          "tipo": "string",
          "desc": "Língua da confirmação: pt, en, es, fr, de ou hu; sem ela, en."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        },
        "novo": {
          "tipo": "bool",
          "desc": "`true` quando o navegador é novo."
        }
      },
      "erros": {
        "400": "assinatura_invalida",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "404": "sem_push: notificação desligada"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/avisos/navegador\", {method: \"POST\", credentials: \"same-origin\", headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf}, body: JSON.stringify({assinatura: inscricao.toJSON(), canal: \"tv-brasil\", lingua: \"pt\"})});",
      "exemploLinguagem": "js",
      "returns": "{ ok, novo }",
      "url": "https://gradetv.net/api/avisos/navegador",
      "auth_detail": "Conta: cookie HttpOnly `__Host-mm-auth`, gravado ao entrar pela modal da conta ou em `/conta/global`; a escrita exige a mesma origem e `X-CSRF-Token` de `/api/auth/bootstrap`. Não há bearer para pessoas."
    },
    {
      "method": "GET",
      "path": "/api/admin/catalogo/estado",
      "auth": "token",
      "summary": "Contagens do catálogo no ar e do staging, mais o carimbo da última recarga.",
      "grupo": "Operação",
      "desc": "Credencial `CATALOGO_TOKEN`. Diagnóstico do estado publicado.",
      "retorno": "EstadoCatalogo",
      "erros": {
        "401": null,
        "503": "`CATALOGO_TOKEN` não configurado no Worker: a recarga está desligada."
      },
      "exemplo": "curl -s $ORIGIN/api/admin/catalogo/estado -H \"Authorization: Bearer $CATALOGO_TOKEN\"",
      "returns": "{ 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} }",
      "url": "https://gradetv.net/api/admin/catalogo/estado",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "GET",
      "path": "/api/admin/catalogo/slugs",
      "auth": "token",
      "summary": "Slug publicado de cada canal, paginado por id — a recarga herda para não trocar URL indexada.",
      "grupo": "Operação",
      "desc": "Keyset por `id`: repita com `apos` = `next_after` até vir `null`.",
      "query": {
        "apos": {
          "tipo": "string",
          "desc": "Cursor: devolve só ids maiores que este (o `next_after` da página anterior).",
          "exemplo": "GloboRJ.br"
        },
        "limit": {
          "tipo": "int",
          "desc": "Tamanho da página; teto de 5000.",
          "exemplo": 5000
        }
      },
      "retorno": "SlugsPublicados",
      "erros": {
        "401": null,
        "503": "Recarga desligada (sem `CATALOGO_TOKEN`)."
      },
      "exemplo": "curl -s \"$ORIGIN/api/admin/catalogo/slugs?limit=5000\" -H \"Authorization: Bearer $CATALOGO_TOKEN\"",
      "returns": "{ items[{id,slug}], next_after }",
      "url": "https://gradetv.net/api/admin/catalogo/slugs",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/catalogo/inicio",
      "auth": "token",
      "summary": "A abertura de staging foi retirada e responde 410.",
      "grupo": "Operação",
      "desc": "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.",
      "retorno": {
        "_texto": "410 `recarga_completa_bloqueada`, após autenticação."
      },
      "erros": {
        "401": null,
        "410": "Sempre: protocolo retirado."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/catalogo/inicio -H \"Authorization: Bearer $CATALOGO_TOKEN\"",
      "returns": "410 `recarga_completa_bloqueada`, após autenticação.",
      "url": "https://gradetv.net/api/admin/catalogo/inicio",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/catalogo/lote",
      "auth": "token",
      "summary": "O envio de lote para staging foi retirado e responde 410.",
      "grupo": "Operação",
      "desc": "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.",
      "retorno": {
        "_texto": "410 `recarga_completa_bloqueada`, após autenticação."
      },
      "erros": {
        "401": null,
        "410": "Sempre: protocolo retirado."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/catalogo/lote -H \"Authorization: Bearer $CATALOGO_TOKEN\"",
      "returns": "410 `recarga_completa_bloqueada`, após autenticação.",
      "url": "https://gradetv.net/api/admin/catalogo/lote",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/guia/lote",
      "auth": "token",
      "summary": "Grava o dia de programação de até 500 canais em `guia_dia` (INSERT OR REPLACE).",
      "grupo": "Operação",
      "desc": "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 128 KiB recusa o pedido com o índice.",
      "corpo": {
        "day": {
          "tipo": "string",
          "desc": "Dia grabado, `YYYY-MM-DD`.",
          "obrigatorio": true
        },
        "canais": {
          "tipo": "object[]",
          "desc": "`{ channel_id, site, programas: [{ inicio, fim, titulo, desc?, categoria? }] }`, instantes ISO 8601. Teto de 500 por pedido.",
          "obrigatorio": true
        }
      },
      "body": {
        "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"
              }
            ]
          }
        ]
      },
      "retorno": "GuiaLoteGravado",
      "erros": {
        "400": "`day` torto, `canais` vazia ou canal inválido (o índice e o motivo vêm na mensagem).",
        "401": null,
        "413": "Mais de 500 canais num pedido."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/guia/lote -H \"Authorization: Bearer $CATALOGO_TOKEN\" -H 'content-type: application/json' -d '{\"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\"}]}]}'",
      "returns": "{ ok, day, gravados }",
      "url": "https://gradetv.net/api/admin/guia/lote",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/guia/fim",
      "auth": "token",
      "summary": "Registra a fonte `guia` em `catalog_meta.fontes`, ao lado das fontes do catálogo.",
      "grupo": "Operação",
      "desc": "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.",
      "corpo": {
        "registro": {
          "tipo": "object",
          "desc": "O mesmo formato de `fontes` da troca: `fetched_at`, `sha256` (pode ser nulo), `itens`, `stale`, `ausente`, `motivo`.",
          "obrigatorio": true
        }
      },
      "body": {
        "registro": {
          "fetched_at": "2026-09-04T03:20:00Z",
          "sha256": null,
          "itens": {
            "canais": 64,
            "sites": 2
          },
          "stale": false,
          "ausente": false
        }
      },
      "retorno": "GuiaRegistrada",
      "erros": {
        "400": "Registro com campo de tipo errado.",
        "401": null
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/guia/fim -H \"Authorization: Bearer $CATALOGO_TOKEN\" -H 'content-type: application/json' -d '{\"registro\":{\"fetched_at\":\"2026-09-04T03:20:00Z\",\"itens\":{\"canais\":64,\"sites\":2},\"stale\":false,\"ausente\":false}}'",
      "returns": "{ ok, guia }",
      "url": "https://gradetv.net/api/admin/guia/fim",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "GET",
      "path": "/api/admin/logos/mortas",
      "auth": "token",
      "summary": "Canais de TV cuja origem de logo morreu (o cron já falhou ao buscá-la) e ainda não têm override.",
      "grupo": "Operação",
      "desc": "Consulta restrita à operação, para identificar logos indisponíveis.",
      "query": {
        "limit": {
          "tipo": "int",
          "desc": "Quantos canais devolver (teto 5000).",
          "padrao": 2000
        }
      },
      "retorno": "LogosMortas",
      "erros": {
        "401": null
      },
      "exemplo": "curl -s \"$ORIGIN/api/admin/logos/mortas?limit=50\" -H \"Authorization: Bearer $CATALOGO_TOKEN\"",
      "returns": "{ items, limit }",
      "url": "https://gradetv.net/api/admin/logos/mortas",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/logos/overrides",
      "auth": "token",
      "summary": "Atualiza o logo exibido na ficha do canal.",
      "grupo": "Operação",
      "desc": "Só https. Sobrevive à recarga (a tabela fica fora da troca). O crédito da fonte sai em `/sobre`.",
      "corpo": {
        "fonte": {
          "tipo": "string",
          "desc": "Identificador para atribuição.",
          "obrigatorio": true
        },
        "itens": {
          "tipo": "object[]",
          "desc": "`{ channel_id, url }`; teto de 500 por pedido.",
          "obrigatorio": true
        }
      },
      "body": {
        "fonte": "licenciante",
        "itens": [
          {
            "channel_id": "BandNews.br",
            "url": "https://logos.example/canal.png"
          }
        ]
      },
      "retorno": "OverridesGravados",
      "erros": {
        "400": "`fonte` torta, lista vazia ou item sem `channel_id`/https (o índice vem na mensagem).",
        "401": null,
        "413": "Mais de 500 itens."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/logos/overrides -H \"Authorization: Bearer $CATALOGO_TOKEN\" -H 'content-type: application/json' -d '{\"fonte\":\"licenciante\",\"itens\":[{\"channel_id\":\"BandNews.br\",\"url\":\"https://logos.example/canal.png\"}]}'",
      "returns": "{ ok, fonte, gravados }",
      "url": "https://gradetv.net/api/admin/logos/overrides",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/catalogo/troca",
      "auth": "token",
      "summary": "A troca integral do catálogo foi retirada e responde 410.",
      "grupo": "Operação",
      "desc": "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.",
      "retorno": {
        "_texto": "410 `recarga_completa_bloqueada`, após autenticação."
      },
      "erros": {
        "401": null,
        "410": "Sempre: protocolo retirado."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/catalogo/troca -H \"Authorization: Bearer $CATALOGO_TOKEN\"",
      "returns": "410 `recarga_completa_bloqueada`, após autenticação.",
      "url": "https://gradetv.net/api/admin/catalogo/troca",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/catalogo/delta/inicio",
      "auth": "token",
      "summary": "Abre a recarga por diferença: coleira contra o catálogo no ar e a marca da execução.",
      "grupo": "Operação",
      "desc": "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.",
      "corpo": {
        "recarga_id": {
          "tipo": "string",
          "desc": "Identificador desta execução (4–64 de `[A-Za-z0-9._-]`); os pedidos seguintes só valem para a recarga que abriu.",
          "obrigatorio": true
        },
        "esperado": {
          "tipo": "object",
          "desc": "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.",
          "obrigatorio": true
        }
      },
      "body": {
        "recarga_id": "2026-09-05T18-00-00Z",
        "esperado": {
          "channels": 93857,
          "channels_fts": 93857,
          "streams": 80734,
          "playable": 9400
        }
      },
      "retorno": "DeltaAberto",
      "erros": {
        "400": "`recarga_id` fora do formato ou `esperado` ausente.",
        "401": null,
        "409": "`delta_recusado`: a resposta lista `problemas[]` e o catálogo no ar não mudou.",
        "429": "`orcamento`: saldo diário insuficiente; preservar a diferença pendente e retomar em outra rodada."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/catalogo/delta/inicio -H \"Authorization: Bearer $CATALOGO_TOKEN\" -H 'content-type: application/json' -d '{\"recarga_id\":\"2026-09-05T18-00-00Z\",\"esperado\":{\"channels\":93857,\"channels_fts\":93857,\"streams\":80734,\"playable\":9400}}'",
      "returns": "{ ok, recarga_id, live{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities} }",
      "url": "https://gradetv.net/api/admin/catalogo/delta/inicio",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/catalogo/delta",
      "auth": "token",
      "summary": "Aplica até 50 linhas de UMA tabela: `upsert` para entradas, `alterar` para colunas modificadas e `remover` por chave.",
      "grupo": "Operação",
      "desc": "`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).",
      "corpo": {
        "recarga_id": {
          "tipo": "string",
          "desc": "Identificador desta execução (4–64 de `[A-Za-z0-9._-]`); os pedidos seguintes só valem para a recarga que abriu.",
          "obrigatorio": true
        },
        "tabela": {
          "tipo": "string",
          "desc": "Uma de `channels`, `channels_fts`, `streams`, `blocklist`, `facet_countries`, `facet_categories`, `facet_languages`, `facet_subdivisions`, `facet_cities`.",
          "obrigatorio": true
        },
        "upsert": {
          "tipo": "object[]",
          "desc": "Linhas com as colunas da tabela; coluna faltando entra com o DEFAULT do schema. Pode ser vazia."
        },
        "alterar": {
          "tipo": "object[]",
          "desc": "`{ chave, valores: { coluna: valor } }`: somente colunas alteradas, com null, string ou número finito. Não aceita chave, slug ou FTS."
        },
        "remover": {
          "tipo": "string[]",
          "desc": "Chaves (`id`, `code` ou `channel_id`, conforme a tabela) a apagar. Pode ser vazia."
        }
      },
      "body": {
        "recarga_id": "2026-09-05T18-00-00Z",
        "tabela": "facet_countries",
        "upsert": [
          {
            "code": "BR",
            "name": "Brazil"
          }
        ],
        "remover": [
          "XX"
        ]
      },
      "retorno": "DeltaAplicado",
      "erros": {
        "400": "Tabela desconhecida, listas ausentes ou as duas vazias, linha sem chave, chave inválida.",
        "401": null,
        "409": "`recarga_divergente`: a execução aberta é outra; chame `/delta/inicio`.",
        "413": "Mais de 50 linhas (`upsert` + `alterar` + `remover`) ou campo textual FTS acima de 4 KiB.",
        "429": "`orcamento`: saldo diário insuficiente; preservar a diferença pendente e retomar em outra rodada."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/catalogo/delta -H \"Authorization: Bearer $CATALOGO_TOKEN\" -H 'content-type: application/json' -d '{\"recarga_id\":\"2026-09-05T18-00-00Z\",\"tabela\":\"facet_countries\",\"upsert\":[{\"code\":\"BR\",\"name\":\"Brazil\"}],\"remover\":[\"XX\"]}'",
      "returns": "{ ok, tabela, recebidas, removidas_pedidas, gravadas, removidas }",
      "url": "https://gradetv.net/api/admin/catalogo/delta",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "POST",
      "path": "/api/admin/catalogo/delta/fim",
      "auth": "token",
      "summary": "Confere o catálogo inteiro contra `esperado` e grava o carimbo (`synced_at`) — só se bateu.",
      "grupo": "Operação",
      "desc": "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.",
      "corpo": {
        "recarga_id": {
          "tipo": "string",
          "desc": "Identificador desta execução (4–64 de `[A-Za-z0-9._-]`); os pedidos seguintes só valem para a recarga que abriu.",
          "obrigatorio": true
        },
        "esperado": {
          "tipo": "object",
          "desc": "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.",
          "obrigatorio": true
        },
        "dump_sha256": {
          "tipo": "string",
          "desc": "SHA-256 do conjunto de dados que gerou este catálogo."
        },
        "origem": {
          "tipo": "string",
          "desc": "Quem recarregou (ex.: `operacao`), para o relatório guardado."
        },
        "resumo": {
          "tipo": "object",
          "desc": "Contagens da diferença (`upsert`, `remover`, `refresh`, `iguais`), só para o relatório."
        }
      },
      "body": {
        "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
        }
      },
      "retorno": "RelatorioDelta",
      "erros": {
        "400": "`recarga_id`, `esperado` ou `fontes` inválidos.",
        "401": null,
        "409": "`recarga_divergente` (outra execução) ou `delta_invalido` — a resposta lista `problemas[]` e o carimbo não foi gravado.",
        "429": "`orcamento`: saldo diário insuficiente; preservar a diferença pendente e retomar em outra rodada."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/admin/catalogo/delta/fim -H \"Authorization: Bearer $CATALOGO_TOKEN\" -H 'content-type: application/json' -d '{\"recarga_id\":\"2026-09-05T18-00-00Z\",\"esperado\":{\"channels\":2,\"channels_fts\":2,\"streams\":2,\"playable\":2},\"origem\":\"manual\"}'",
      "returns": "{ ok, recarga_id, depois{channels,channels_fts,channels_fts_ids,streams,blocklist,facet_countries,facet_categories,facet_languages,facet_subdivisions,facet_cities}, synced_at }",
      "url": "https://gradetv.net/api/admin/catalogo/delta/fim",
      "auth_detail": "Token de operador: `METRICS_TOKEN` nas métricas e relatos crus; `CATALOGO_TOKEN` nas rotas `/api/admin/catalogo/*` da recarga (credencial exclusiva de operação)."
    },
    {
      "method": "GET",
      "path": "/api/credito/x402/saude",
      "auth": "none",
      "exemplo": "curl -s $ORIGIN/api/credito/x402/saude",
      "grupo": "Crédito",
      "summary": "Saúde do verificador x402: pendências, falta de hash e falha ou parada da conciliação.",
      "retorno": {
        "verificado_em": {
          "tipo": "string",
          "desc": "Hora da leitura, em UTC."
        },
        "ativo": {
          "tipo": "boolean",
          "desc": "Verificador configurado no processo."
        },
        "tarefas": {
          "tipo": "object[]",
          "desc": "Falhas com id e motivo, sem dados do pagador. Contagem limitada indica +."
        }
      },
      "erros": {
        "503": "Verificador indisponível neste processo."
      },
      "returns": "{ verificado_em, ativo, tarefas }",
      "url": "https://gradetv.net/api/credito/x402/saude",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/credito/barrados/saude",
      "auth": "none",
      "exemplo": "curl -s $ORIGIN/api/credito/barrados/saude",
      "grupo": "Crédito",
      "summary": "Quem pagou e foi barrado neste site nas últimas 24 horas: a casa pediu pagamento a quem tinha saldo ou plano, ou cobrou e não entregou.",
      "retorno": {
        "verificado_em": {
          "tipo": "string",
          "desc": "Hora da leitura, em UTC."
        },
        "ativo": {
          "tipo": "boolean",
          "desc": "A leitura funcionou."
        },
        "total": {
          "tipo": "int",
          "desc": "Quantas ocorrências diferentes houve."
        },
        "tarefas": {
          "tipo": "object[]",
          "desc": "Até 8 ocorrências, com classe, rota, vezes e última hora, sem dados de quem pagou."
        }
      },
      "returns": "{ verificado_em, ativo, total, tarefas }",
      "url": "https://gradetv.net/api/credito/barrados/saude",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/credito/conta",
      "auth": "credito",
      "exemplo": "curl -s $ORIGIN/api/credito/conta -H 'Cookie: <sessão>'",
      "grupo": "Crédito",
      "summary": "O saldo da conta da sessão: a soma das compras dela neste ambiente, em todo produto, sem precisar do código. Com movimentos=1, o extrato da conta.",
      "query": {
        "movimentos": {
          "tipo": "string",
          "desc": "`1`: consulta só o extrato paginado da conta, sem saldo."
        },
        "de": {
          "tipo": "string",
          "desc": "Início inclusivo em UTC ISO 8601 com milissegundos. Padrão: 1970-01-01T00:00:00.000Z."
        },
        "ate": {
          "tipo": "string",
          "desc": "Fim exclusivo em UTC ISO 8601 com milissegundos. Padrão: agora. Conserve o valor entre páginas."
        },
        "antes": {
          "tipo": "string",
          "desc": "Cursor `proximo` da página anterior. Só com movimentos=1."
        },
        "limite": {
          "tipo": "int",
          "desc": "1 a 50 movimentos por página; padrão 20. Só com movimentos=1."
        }
      },
      "retorno": {
        "saldo_micros": {
          "tipo": "int",
          "desc": "O saldo em milionésimos de dólar."
        },
        "saldo_usd": {
          "tipo": "string",
          "desc": "O saldo em dólar, formatado."
        },
        "referencia_usd": {
          "tipo": "string",
          "desc": "O saldo logo depois da última recarga, somado nas carteiras com saldo: a base do que resta."
        },
        "cotacao_brl_por_usd": {
          "tipo": "number",
          "desc": "A cotação do real que a tela usa para mostrar o saldo."
        },
        "movimentos": {
          "tipo": "object[]",
          "desc": "Só com movimentos=1: id, em, valor_usd, saldo_usd, motivo, produto e recurso, do mais novo."
        },
        "proximo": {
          "tipo": "string",
          "desc": "Só com movimentos=1: cursor da próxima página ou null."
        }
      },
      "erros": {
        "400": "Período, limite ou cursor inválido.",
        "401": "Sem sessão."
      },
      "returns": "{ saldo_micros, saldo_usd, referencia_usd, cotacao_brl_por_usd, movimentos, proximo }",
      "url": "https://gradetv.net/api/credito/conta",
      "auth_detail": "Token de crédito em `Authorization: Bearer cred_…` (ou `X-Credito`). Para saldo recuperado de x402, use a credencial de carteira `mmw_…` em Bearer ou `X-Wallet-Token`, emitida por `/api/auth/wallet/agent/start` e `/api/auth/wallet/agent/token`."
    },
    {
      "method": "POST",
      "path": "/api/credito/conta",
      "auth": "credito",
      "exemplo": "curl -s -XPOST $ORIGIN/api/credito/conta -H 'Authorization: Bearer cred_…' -H 'Cookie: <sessão>'",
      "grupo": "Crédito",
      "summary": "Liga à conta da sessão o crédito comprado sem conta, inclusive em USDC, com o plano de consultas pago por ele. Depois disso, o saldo vale nos produtos do crédito da casa com `X-Credito: conta`, somado ao das outras compras da conta.",
      "retorno": {
        "ok": {
          "tipo": "boolean",
          "desc": "A ligação foi conferida."
        },
        "ligadas": {
          "tipo": "int",
          "desc": "Compras ligadas agora. Zero quando a compra já tem conta."
        },
        "da_conta": {
          "tipo": "boolean",
          "desc": "O saldo deste código já vem no saldo da conta."
        }
      },
      "erros": {
        "401": "Sem sessão ou sem o código de crédito."
      },
      "returns": "{ ok, ligadas, da_conta }",
      "url": "https://gradetv.net/api/credito/conta",
      "auth_detail": "Token de crédito em `Authorization: Bearer cred_…` (ou `X-Credito`). Para saldo recuperado de x402, use a credencial de carteira `mmw_…` em Bearer ou `X-Wallet-Token`, emitida por `/api/auth/wallet/agent/start` e `/api/auth/wallet/agent/token`."
    },
    {
      "method": "POST",
      "path": "/api/credito",
      "auth": "none",
      "summary": "Recarrega crédito pré-pago: paga uma vez com x402 e recebe o token que desconta em qualquer API da casa.",
      "grupo": "Crédito",
      "preco_usd_faixa": [
        0.1,
        25
      ],
      "query": {
        "usd": {
          "tipo": "number",
          "desc": "Pacote: 0.1, 1, 5, 10 ou 25 dólares.",
          "obrigatorio": true,
          "enum": [
            0.1,
            1,
            5,
            10,
            25
          ]
        }
      },
      "retorno": {
        "token": {
          "tipo": "string",
          "desc": "Token portador do saldo (`cred_…`). Mostrado UMA vez — não há como recuperá-lo."
        },
        "saldo_usd": {
          "tipo": "string",
          "desc": "Saldo creditado."
        },
        "guarde": {
          "tipo": "string",
          "desc": "Aviso de que o token é o portador do crédito."
        },
        "usar": {
          "tipo": "string",
          "desc": "Como apresentar o token nas rotas pagas."
        },
        "usar_com_outra_credencial": {
          "tipo": "string",
          "desc": "O header `X-Credito`, para quando o `Authorization` já leva outra credencial."
        },
        "saldo_em": {
          "tipo": "string",
          "desc": "Onde consultar saldo e extrato."
        },
        "onde_gastar": {
          "tipo": "object",
          "desc": "`precos`: as ações pagas e os preços deste produto (`/api/pricing`); `produtos`: o APIs.json com os outros produtos da casa."
        }
      },
      "erros": {
        "400": "Pacote fora da lista (0.1, 1, 5, 10 ou 25).",
        "402": "Sem pagamento — o corpo traz `accepts[]` do x402."
      },
      "exemplo": "curl -s -XPOST '$ORIGIN/api/credito?usd=10'",
      "returns": "{ token, saldo_usd, guarde, usar, usar_com_outra_credencial, saldo_em, onde_gastar }",
      "url": "https://gradetv.net/api/credito",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/credito",
      "auth": "credito",
      "summary": "Saldo e extrato com cred_…; com mmw_…, saldo da carteira e até 20 pagamentos em recuperação.",
      "desc": "Sem token, responde 401 e aponta o manifesto x402 no cabeçalho Link. Com a cobrança configurada, o corpo inclui accepts[] com os mesmos termos da menor recarga por POST. O GET não cobra nem abre crédito.",
      "grupo": "Crédito",
      "query": {
        "movimentos": {
          "tipo": "string",
          "desc": "`1`: consulta só o histórico paginado, sem saldo."
        },
        "de": {
          "tipo": "string",
          "desc": "Início inclusivo em UTC ISO 8601 com milissegundos. Padrão: 1970-01-01T00:00:00.000Z."
        },
        "ate": {
          "tipo": "string",
          "desc": "Fim exclusivo em UTC ISO 8601 com milissegundos. Padrão: agora. Conserve o valor entre páginas."
        },
        "antes": {
          "tipo": "string",
          "desc": "Cursor `proximo` da página anterior. Só com movimentos=1."
        },
        "limite": {
          "tipo": "int",
          "desc": "1 a 50 movimentos por página; padrão 20. Só com movimentos=1."
        }
      },
      "retorno": {
        "saldo_micros": {
          "tipo": "int",
          "desc": "Saldo em micro-dólares (1e-6 USD)."
        },
        "saldo_usd": {
          "tipo": "string",
          "desc": "Saldo formatado."
        },
        "criado_em": {
          "tipo": "string",
          "desc": "Quando o crédito foi aberto."
        },
        "movimentos": {
          "tipo": "object[]",
          "desc": "Entradas e saídas recentes, com produto e recurso."
        },
        "wallet": {
          "tipo": "string",
          "desc": "Só com mmw_…: endereço autenticado da carteira."
        },
        "payments": {
          "tipo": "object[]",
          "desc": "Só com mmw_…: até 20 pagamentos, com id, produto, rede, tx_hash, micros, estado e criado_ms. creditado indica saldo recuperado."
        },
        "proximo": {
          "tipo": "string|null",
          "desc": "Só com movimentos=1: cursor da próxima página ou null; movimentos inclui id, em, valor_usd, saldo_usd, motivo, produto e recurso."
        },
        "onde_gastar": {
          "tipo": "object",
          "desc": "`precos`: as ações pagas e os preços deste produto; `produtos`: o APIs.json com os outros produtos da casa."
        }
      },
      "erros": {
        "400": "Período, limite ou cursor inválido.",
        "401": "Sem token ou token desconhecido."
      },
      "exemplo": "curl -s $ORIGIN/api/credito -H 'Authorization: Bearer cred_…'",
      "returns": "{ saldo_micros, saldo_usd, criado_em, movimentos, wallet, payments, proximo, onde_gastar }",
      "url": "https://gradetv.net/api/credito",
      "auth_detail": "Token de crédito em `Authorization: Bearer cred_…` (ou `X-Credito`). Para saldo recuperado de x402, use a credencial de carteira `mmw_…` em Bearer ou `X-Wallet-Token`, emitida por `/api/auth/wallet/agent/start` e `/api/auth/wallet/agent/token`."
    },
    {
      "method": "GET",
      "path": "/api/credito/pix",
      "auth": "none",
      "summary": "Crédito por Pix: a chave, o câmbio fixo, os pacotes em reais e o que o comprovante aceita.",
      "grupo": "Crédito",
      "retorno": {
        "chave": {
          "tipo": "string",
          "desc": "A chave Pix que recebe o pagamento."
        },
        "brl_por_usd": {
          "tipo": "number",
          "desc": "Câmbio fixo usado nos pacotes."
        },
        "pacotes": {
          "tipo": "object[]",
          "desc": "Os pacotes (3, 5, 10 ou 25 dólares), cada um com `brl_centavos` e `copia_e_cola` (o Pix copia e cola do valor, o mesmo texto do QR: o app do banco já vem com o valor)."
        },
        "comprovante": {
          "tipo": "object",
          "desc": "Tipos aceitos (foto ou PDF) e o tamanho máximo, em bytes."
        },
        "liberacao": {
          "tipo": "string",
          "desc": "`manual`: o dono confere o Pix e libera."
        }
      },
      "exemplo": "curl -s $ORIGIN/api/credito/pix",
      "returns": "{ chave, brl_por_usd, pacotes, comprovante, liberacao }",
      "url": "https://gradetv.net/api/credito/pix",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/credito/pix",
      "auth": "none",
      "summary": "Pede crédito pago por Pix: multipart com `usd`, `comprovante` (foto ou PDF até 2 MB) e `email`; o resto é opcional.",
      "desc": "O código de crédito sai na resposta e passa a valer quando o dono confere o Pix e libera (manual, em geral no mesmo dia). O `email` é obrigatório: é por ele que o dono fala com a pessoa. Opcionais: `nome`, `pagina` (a página de onde pediu, até 2.000 caracteres) e o que a pessoa comprava quando recebeu o 402 — `recurso` (até 2.000), `descricao` (até 300) e `preco_usd` (decimal, ex. `0.50`); e `navegador_id` (UUID que liga os pedidos do mesmo navegador). Tudo o que chega fica registrado no pedido — o comprovante inclusive —, com a conta logada (conferida pelo cookie da sessão), a rede e o navegador de quem pediu.",
      "grupo": "Crédito",
      "retorno": {
        "id": {
          "tipo": "string",
          "desc": "Id do pedido, para acompanhar."
        },
        "estado": {
          "tipo": "string",
          "desc": "`pendente` até a decisão."
        },
        "credito": {
          "tipo": "string",
          "desc": "O token `cred_…`, mostrado UMA vez; vale depois da liberação."
        },
        "estado_em": {
          "tipo": "string",
          "desc": "Onde acompanhar o pedido."
        }
      },
      "erros": {
        "400": "Pacote fora da lista (3, 5, 10 ou 25), sem comprovante ou sem e-mail válido.",
        "413": "Comprovante acima de 2 MB.",
        "415": "Comprovante que não é foto (JPEG, PNG, WebP) nem PDF.",
        "429": "A rede já mandou os pedidos do dia.",
        "502": "O e-mail ao dono não saiu: o pedido fica registrado como `falhou`; mande de novo.",
        "503": "Fila de conferência cheia, ou Pix indisponível neste app."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/credito/pix -F usd=5 -F email=voce@empresa.com.br -F comprovante=@pix.pdf",
      "returns": "{ id, estado, credito, estado_em }",
      "url": "https://gradetv.net/api/credito/pix",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/credito/pix/:id",
      "auth": "none",
      "summary": "Estado de um pedido de crédito por Pix: `pendente`, `liberado`, `recusado` ou `falhou`.",
      "grupo": "Crédito",
      "params": {
        "id": {
          "desc": "Id do pedido (32 hex).",
          "exemplo": "0123456789abcdef0123456789abcdef"
        }
      },
      "retorno": {
        "estado": {
          "tipo": "string",
          "desc": "`pendente`, `liberado`, `recusado` ou `falhou` (o e-mail ao dono não saiu; mande de novo)."
        },
        "usd": {
          "tipo": "int",
          "desc": "O pacote pedido."
        },
        "decidido_em": {
          "tipo": "string",
          "desc": "Quando o dono decidiu, ou `null`."
        }
      },
      "erros": {
        "404": "Pedido desconhecido."
      },
      "exemplo": "curl -s $ORIGIN/api/credito/pix/0123456789abcdef0123456789abcdef",
      "returns": "{ estado, usd, decidido_em }",
      "url": "https://gradetv.net/api/credito/pix/:id",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/credito/asaas/:id/cancelar",
      "auth": "none",
      "grupo": "Crédito",
      "summary": "Cancela uma cobrança pendente ao fechar o pagamento.",
      "desc": "Exige o bilhete `cancelamento` devolvido na criação. Uma cobrança antiga de crédito aceita seu token em `credito`. Só atua no produto e ambiente de origem. Não estorna pagamentos nem remove o histórico. 202 indica cancelamento em andamento; a vigia retoma falhas externas.",
      "params": {
        "id": {
          "desc": "Id da cobrança (32 hex).",
          "exemplo": "0123456789abcdef0123456789abcdef"
        }
      },
      "retorno": {
        "id": {
          "tipo": "string",
          "desc": "Id da cobrança."
        },
        "estado": {
          "tipo": "string",
          "desc": "`cancelando`, `vencido` ou o estado final já confirmado."
        }
      },
      "erros": {
        "404": "Cobrança ou bilhete inválido, produto ou ambiente diferente."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/credito/asaas/0123456789abcdef0123456789abcdef/cancelar -H 'content-type: application/json' -d '{\"cancelamento\":\"<bilhete>\"}'",
      "returns": "{ id, estado }",
      "url": "https://gradetv.net/api/credito/asaas/:id/cancelar",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/credito/asaas",
      "auth": "none",
      "summary": "Pix automático: se está disponível neste app, os pacotes em reais e o saldo que o produto vende.",
      "grupo": "Crédito",
      "retorno": {
        "disponivel": {
          "tipo": "boolean",
          "desc": "`false` quando o app não tem o Asaas configurado (use o Pix com comprovante)."
        },
        "pacotes": {
          "tipo": "object[]",
          "desc": "Os pacotes de crédito (3, 5, 10 ou 25 dólares), com `brl_centavos`."
        },
        "exatos": {
          "tipo": "object[]",
          "desc": "Preços declarados pelo app, com `usd` e `brl_centavos`. Só aparecem a partir de R$ 5,00 e até R$ 10.000."
        },
        "saldo": {
          "tipo": "object",
          "desc": "Quando o produto vende saldo em reais: a faixa e se a conta logada pode comprar."
        },
        "precisa_documento": {
          "tipo": "boolean",
          "desc": "Se o CPF ou CNPJ de quem paga ainda é pedido."
        }
      },
      "exemplo": "curl -s $ORIGIN/api/credito/asaas",
      "returns": "{ disponivel, pacotes, exatos, saldo, precisa_documento }",
      "url": "https://gradetv.net/api/credito/asaas",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/credito/asaas",
      "auth": "none",
      "summary": "Gera um Pix dinâmico: JSON com `oferta` (`pacote` com `usd`, ou `saldo` com `centavos`) e o pagador.",
      "desc": "O pagamento libera sozinho o que se comprou: o pacote vira o token `cred_…` que já vem na resposta; o saldo cai na organização da conta logada. Na primeira compra vão `documento` (CPF ou CNPJ, que segue para o gateway e não fica guardado aqui) e `nome`; `email`, `pagina` e `navegador_id` são opcionais e ficam registrados. Com `forma: \"cartao_boleto\"`, a cobrança é livre: a resposta traz `pagamento_em`, a página do gateway onde se paga com cartão, boleto ou Pix (vence em três dias). Com `forma: \"cartao\"` e o `cartao` (`titular`, `numero`, `mes`, `ano`, `cvv`, `documento`, `email`, `cep`, `numero_endereco`, `telefone`), a cobrança é paga no cartão no mesmo pedido: 200 com `estado: \"pago\"`, 202 com `conferir` enquanto o emissor analisa, 422 quando o cartão é recusado; o cartão não fica guardado. O `usd` deve ser um valor de `pacotes` ou `exatos` do GET. O servidor define os centavos e recusa valores não declarados.",
      "grupo": "Crédito",
      "retorno": {
        "id": {
          "tipo": "string",
          "desc": "Id da cobrança, para acompanhar."
        },
        "credito": {
          "tipo": "string",
          "desc": "No pacote: o token `cred_…`, mostrado UMA vez; vale quando o pagamento cair."
        },
        "forma": {
          "tipo": "string",
          "desc": "`pix` (o padrão), `cartao` ou `cartao_boleto`."
        },
        "pix": {
          "tipo": "object",
          "desc": "`copia_e_cola`, `imagem` (QR em PNG, data URI) e `expira_em`; só na forma `pix`."
        },
        "pagamento_em": {
          "tipo": "string",
          "desc": "Na forma `cartao_boleto`: a página de pagamento do gateway."
        },
        "estado": {
          "tipo": "string",
          "desc": "Na forma `cartao`: `pago`, ou `pendente` com `conferir`."
        },
        "estado_em": {
          "tipo": "string",
          "desc": "Onde acompanhar o pagamento."
        },
        "cancelamento": {
          "tipo": "string",
          "desc": "Bilhete para cancelar a cobrança pendente de Pix ou boleto."
        }
      },
      "erros": {
        "400": "Crédito fora de `pacotes` e `exatos`, valor fora da faixa, CPF/CNPJ, nome ou e-mail inválido, dado do cartão inválido (`cartao_dados`, com o `campo`), ou o gateway recusou o pagador (`pagador_recusado`, com a razão em `detalhe`).",
        "422": "Cartão recusado (`cartao_recusado`, com o `campo` quando o gateway diz qual); troque em `POST /api/credito/asaas/:id/cartao`.",
        "429": "Limite de 3 tentativas por minuto ou 10 por dia, por conta ou rede, ou limite do cartão.",
        "502": "O gateway não gerou o Pix; tente de novo.",
        "503": "Pix automático indisponível neste app, o gateway ocupado (`asaas_ocupado`, espere `retry_after_s`), ou cobranças demais de quem não entrou esperando pagamento."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/credito/asaas -H 'content-type: application/json' -d '{\"oferta\":\"pacote\",\"usd\":5,\"documento\":\"000.000.000-00\",\"nome\":\"Ana\"}'",
      "returns": "{ id, credito, forma, pix, pagamento_em, estado, estado_em, cancelamento }",
      "url": "https://gradetv.net/api/credito/asaas",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/credito/asaas/:id",
      "auth": "none",
      "summary": "Estado de uma cobrança Pix automática: `pendente`, `pago`, `vencido`, `estornado` ou `falhou`.",
      "grupo": "Crédito",
      "params": {
        "id": {
          "desc": "Id da cobrança (32 hex).",
          "exemplo": "0123456789abcdef0123456789abcdef"
        }
      },
      "retorno": {
        "estado": {
          "tipo": "string",
          "desc": "`pago` libera o token do pacote ou o saldo; o Pix pago depois do vencimento vale."
        },
        "pago_em": {
          "tipo": "string",
          "desc": "Quando o pagamento foi confirmado, ou `null`."
        }
      },
      "erros": {
        "404": "Cobrança desconhecida."
      },
      "exemplo": "curl -s $ORIGIN/api/credito/asaas/0123456789abcdef0123456789abcdef",
      "returns": "{ estado, pago_em }",
      "url": "https://gradetv.net/api/credito/asaas/:id",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/credito/asaas/:id/cartao",
      "auth": "none",
      "summary": "Paga com outro cartão a cobrança de cartão cujo cartão foi recusado: a mesma cobrança, sem cobrar duas vezes.",
      "desc": "JSON com o `cartao` (os mesmos campos do `POST /api/credito/asaas`) e, sem conta, o `navegador_id` que criou a cobrança. Só a cobrança de cartão que espera pagamento, de quem a criou, e até cinco tentativas por cobrança.",
      "grupo": "Crédito",
      "params": {
        "id": {
          "desc": "Id da cobrança (32 hex).",
          "exemplo": "0123456789abcdef0123456789abcdef"
        }
      },
      "retorno": {
        "estado": {
          "tipo": "string",
          "desc": "`pago`, ou `pendente` com `conferir` enquanto o emissor analisa."
        },
        "estado_em": {
          "tipo": "string",
          "desc": "Onde acompanhar o pagamento."
        }
      },
      "erros": {
        "400": "Dado do cartão inválido (`cartao_dados`, com o `campo`).",
        "404": "Cobrança desconhecida, de outra pessoa ou que não é de cartão.",
        "409": "A cobrança já venceu ou foi encerrada; comece o pagamento de novo.",
        "422": "Cartão recusado (`cartao_recusado`).",
        "429": "Passou das tentativas de cartão desta cobrança ou do dia."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/credito/asaas/0123456789abcdef0123456789abcdef/cartao -H 'content-type: application/json' -d '{\"cartao\":{…}}'",
      "returns": "{ estado, estado_em }",
      "url": "https://gradetv.net/api/credito/asaas/:id/cartao",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/credito/asaas/saude",
      "auth": "none",
      "summary": "A saúde do Pix e das assinaturas ativas deste app: falhas com motivo, para o painel.",
      "grupo": "Crédito",
      "retorno": {
        "ativo": {
          "tipo": "boolean",
          "desc": "`false` quando o Asaas e as assinaturas estão desligados."
        },
        "tarefas": {
          "tipo": "object[]",
          "desc": "Cada problema: `id`, `falhando` e `motivo`. Inclui Pix e, quando ativas, recuperação, fila e orçamento das assinaturas. Não consulta o provedor."
        }
      },
      "exemplo": "curl -s $ORIGIN/api/credito/asaas/saude",
      "returns": "{ ativo, tarefas }",
      "url": "https://gradetv.net/api/credito/asaas/saude",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/pricing",
      "auth": "none",
      "grupo": "Descoberta",
      "summary": "Preços vigentes e franquias gratuitas.",
      "retorno": {
        "product": {
          "tipo": "string",
          "desc": "Product name."
        },
        "quota": {
          "tipo": "PaymentQuota",
          "desc": "Public allowances and current list prices; not personal usage."
        },
        "pricing": {
          "tipo": "string",
          "desc": "Absolute URL of the current price list."
        },
        "billing": {
          "tipo": "string",
          "desc": "Absolute URL of payment discovery or the existing billing summary."
        },
        "api_index": {
          "tipo": "string",
          "desc": "Absolute URL of the API catalog."
        }
      },
      "erros": {
        "405": "Use GET ou HEAD."
      },
      "exemplo": "curl -s $ORIGIN/api/pricing",
      "returns": "{ product, quota{free,paid,how_to_pay,live,free_now?,charged_now?,trial?}, pricing, billing, api_index }",
      "url": "https://gradetv.net/api/pricing",
      "auth_detail": "Público, sem credencial."
    }
  ],
  "quota": {
    "free": [
      {
        "o_que": "catálogo, busca e filtros (`GET /api/channels`)",
        "limite": "sem franquia de consultas",
        "janela": null
      },
      {
        "o_que": "pastas pessoais e listas M3U/JSON/XSPF",
        "limite": "8 pastas, 12 sub-abas por pasta e 40 canais por sub-aba",
        "janela": null
      },
      {
        "o_que": "ler chat, comentários e saúde de canal",
        "limite": "sem cota",
        "janela": null
      },
      {
        "o_que": "programação e histórico por canal e dia",
        "limite": "sem cota",
        "janela": null
      },
      {
        "o_que": "HLS ao vivo pelo servidor de listas",
        "limite": "120 minutos por rede/dia UTC, compartilhados entre canais e aparelhos",
        "janela": null
      }
    ],
    "paid": [
      {
        "o_que": "escrever no chat (30 dias)",
        "price_usd": 0.1
      },
      {
        "o_que": "TV em 2 aparelhos (30 dias, sem renovação automática)",
        "price_usd": 0.1
      },
      {
        "o_que": "TV em 2 aparelhos (90 dias, sem renovação automática)",
        "price_usd": 0.29
      },
      {
        "o_que": "TV em 2 aparelhos (365 dias, sem renovação automática)",
        "price_usd": 0.99
      },
      {
        "o_que": "5 aparelhos por 30 dias",
        "price_usd": 0.2
      },
      {
        "o_que": "10 aparelhos por 30 dias",
        "price_usd": 0.35
      },
      {
        "o_que": "15 aparelhos por 30 dias",
        "price_usd": 0.5
      },
      {
        "o_que": "Uma vaga adicional por 30 dias",
        "price_usd": 0.05
      },
      {
        "o_que": "GET /api/x402/programacao/:slug — Os últimos sete dias de programação do canal numa chamada, para o agente, pagos por chamada, sem conta.",
        "price_usd": 0.01
      },
      {
        "o_que": "GET /api/x402/programacao/:slug/:dia — Sete dias de programação do canal que terminam no dia pedido, para o agente, pagos por chamada, sem conta.",
        "price_usd": 0.01
      }
    ],
    "how_to_pay": "Rota paga responde **402** com `accepts[]` (x402, USDC na Base). Pague e repita a mesma chamada com `PAYMENT-SIGNATURE` (v2) ou `X-PAYMENT` (v1); um cliente x402 padrão, como o `@x402/fetch`, faz isso sozinho.",
    "live": "https://gradetv.net/api/billing"
  },
  "mcp": {
    "endpoint": "https://gradetv.net/mcp",
    "transport": "streamable-http",
    "tools": 46,
    "note": "MCP significa Model Context Protocol. Conecte este endereço ao seu cliente MCP. As ferramentas disponíveis executam as operações listadas nesta API."
  },
  "quickstart": [
    "POST https://gradetv.net/api/guest → ipt_…",
    "GET https://gradetv.net/api/channels?country=BR&playable=1",
    "GET https://gradetv.net/api/library com X-Guest-Token: ipt_…"
  ]
}