{
  "name": "Grade",
  "description": "Catálogo pesquisável de transmissões públicas e biblioteca pessoal endereçável: categorias, sub-abas, feeds M3U/JSON/XSPF. Stream no M3U/API é https://m3m8.gradetv.net/api/s/:id (refresh HLS não passa pelo Worker; o player conta o play uma vez no apex). Playlists pela Grade, mídia direta da origem. O Grade não retransmite vídeo. Projetos de transmissão autorizada para produtores são recebidos sob consulta em /api/producers.",
  "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"
      }
    ],
    "pages": {
      "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 header `X-Credito`). Não é conta: é portador de saldo.",
    "none": "Público, sem credencial.",
    "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://staging.gradetv.net/llms.txt",
    "llms_full": "https://staging.gradetv.net/llms-full.txt",
    "openapi": "https://staging.gradetv.net/openapi.json",
    "mcp": "https://staging.gradetv.net/mcp",
    "pricing": "https://staging.gradetv.net/api/pricing",
    "billing": "https://staging.gradetv.net/api/billing",
    "human_ui": "https://staging.gradetv.net/",
    "data_indexes": [
      {
        "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.",
        "hierarchy": "UF → município → bairro/localidade → rua → endereços",
        "url": "https://api.pontofato.com/enderecos/",
        "formats": {
          "html": "https://api.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.",
        "hierarchy": "Modalidade → UF → ano → mês → dia → município → compras",
        "url": "https://api.editalmd.com/licitacoes/",
        "formats": {
          "html": "https://api.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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": "globo"
        },
        "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.",
          "exemplo": "Globo"
        },
        "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://staging.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://staging.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, codes, _links{self,channel,report} }",
      "url": "https://staging.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,desc?,categoria?}, a_seguir{inicio,fim,titulo,desc?,categoria?}, programas[{inicio,fim,titulo,desc?,categoria?}] }",
      "url": "https://staging.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.",
      "grupo": "Catálogo",
      "retorno": "Geo",
      "exemplo": "curl -s $ORIGIN/api/geo",
      "returns": "{ country, detected, language, language_detected, source, api }",
      "url": "https://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.gradetv.net/api/cities",
      "auth_detail": "Público, sem credencial."
    },
    {
      "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 por API mantém o gate x402 ou crédito pré-pago; o formulário humano usa Turnstile.",
      "query": {
        "lang": {
          "tipo": "string",
          "desc": "Idioma da oferta: pt, en, es, fr ou de; ausente ou desconhecido volta a pt.",
          "padrao": "pt",
          "valores": [
            "pt",
            "en",
            "es",
            "fr",
            "de"
          ]
        }
      },
      "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://staging.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 R2, ou responde 404. É o que impede a página de card virar um proxy de imagem de terceiro.",
      "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."
      },
      "returns": "`image/webp` ou `image/png` — os bytes do logo.",
      "url": "https://staging.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 cache de 1 s na CDN (browser continua no-store). Links antigos em gradetv.net/api/s/ ainda 302 no-store para o mesmo path. O Grade NÃO retransmite vídeo: os segmentos e chaves vêm da origem, no browser e no VLC. Playlists internas usam o mesmo hop com `p` assinado; copie a URL completa, sem montar o ticket. Cada busca tem teto de 512 KiB, 8 s e 3 redirects revalidados; até 128 referências internas por manifesto e 4 níveis. Refresh interno não conta outro play. Falha de playlist no o2 é 422 JSON (a CDN não reescreve 4xx); o Worker de fallback ainda usa 502. Sem redirecionamento para HTTP. Headers expostos por CORS: X-Grade-Upstream-Status (status observado na origem), X-Grade-Source (URL original da transmissão para copiar), X-Grade-Website (site oficial do canal, vazio quando ausente), X-Grade-Legacy (ação HTTP pública) e X-Grade-Media-HTTP (1 se este manifesto referencia mídia/chave HTTP, 0 caso contrário). Mídia HTTP oferece player legado isolado; 401/403 da origem oferece abrir o site do canal e copiar a playlist original para outro player. CORS ainda pode impedir reprodução. Áudio/vídeo contínuo e YouTube recebem 302 para a origem. IDs novos não dependem da ordem da recarga. Um ID posicional antigo ausente recupera um stream do mesmo canal, feed e fonte, preservando os bloqueios.",
      "params": {
        "id": {
          "desc": "ID do stream, que vem em `Stream.id`.",
          "exemplo": "1001Noites.br:SD:2168"
        }
      },
      "query": {
        "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://staging.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:8080/legacy?stream=ID. A página isolada aceita lang=pt/en/es/fr/de e theme=light/dark. Playlists continuam pelo hop HTTPS e mídia direto da origem. Não remove recusa de acesso nem exigência CORS. Uma leitura indexada; não busca origem nem grava no D1.",
      "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."
        },
        "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, name, kind, radio, url, provider_url, website, legacy_url }",
      "url": "https://staging.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://staging.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 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"
          ],
          "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\"`."
      ],
      "retorno": {
        "_texto": "`audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`."
      },
      "erros": {
        "404": "Token de feed desconhecido."
      },
      "exemplo": "curl -s $ORIGIN/f/FEED_TOKEN/library.m3u",
      "returns": "`audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`.",
      "url": "https://staging.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"
          ],
          "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\"`."
      ],
      "retorno": {
        "_texto": "`audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`."
      },
      "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` ou `application/xspf+xml`.",
      "url": "https://staging.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"
          ],
          "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\"`."
      ],
      "retorno": {
        "_texto": "`audio/x-mpegurl` (m3u/m3u8), `application/json` ou `application/xspf+xml`."
      },
      "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` ou `application/xspf+xml`.",
      "url": "https://staging.gradetv.net/f/:token/c/:categoria/g/:grupo.:formato",
      "auth_detail": "Público, sem credencial."
    },
    {
      "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://staging.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://staging.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://staging.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://staging.gradetv.net/api/keys/:id",
      "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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://staging.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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, name, slug, group_id }",
      "url": "https://staging.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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://staging.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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://staging.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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, name, slug, category_id }",
      "url": "https://staging.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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://staging.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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}], id, group_id, category_id }",
      "url": "https://staging.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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://staging.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}, categories[{id,name,slug,sort,last_group_id,api,feeds,groups}] }",
      "url": "https://staging.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://staging.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://staging.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://staging.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://staging.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",
      "auth": "guest",
      "summary": "Relata se o canal tocou ou falhou — é o que alimenta a saúde pública do catálogo.",
      "grupo": "Saúde",
      "desc": "Conta uma vez por dono, por canal, por dia e por resultado; relatar de novo devolve 200 com `reason: ja_relatado_hoje`, e não é erro. Navegador e sistema saem do User-Agent e o país da borda — mandar isso no corpo não muda nada. Sem relato, o catálogo não aprende: é assim que `GET /api/channels/:id/health` sabe distinguir canal fora do ar de canal bloqueado para você.",
      "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": "Por que falhou; só quando `ok` é `false`.",
          "valores": [
            "cors",
            "geo",
            "sumiu",
            "codec",
            "playlist",
            "sem_resposta",
            "protocolo",
            "sem_stream",
            "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": "`channel_id` ausente, `ok` faltando ou `code` fora da lista.",
        "401": null
      },
      "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://staging.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": "Relatos crus, com endereço IP, para investigar um canal — só operador.",
      "grupo": "Saúde",
      "desc": "Existe separado do painel público justamente porque traz endereço. A linha some depois do prazo em `retention_days`, e `/api/channels/:id/health` nunca devolve IP.",
      "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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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); `null` com o chat aberto de graça (`X402_GRATIS`).",
          "nulo": true
        }
      },
      "erros": {
        "401": null,
        "402": null,
        "410": "Chave `iptk_…` (retirada): use o convidado.",
        "503": "`pass_unavailable` (sem registro de compras, nada cobrado) ou `pass_not_recorded` (pago, não gravado: traz o recibo)."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/chat/pass -H \"X-Guest-Token: $IPT\" -H \"X-PAYMENT: $PAGAMENTO\"",
      "returns": "{ ok, charged, until }",
      "url": "https://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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": "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://staging.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 nos últimos 5 minutos; a de organização também exige segundo fator na sessão e o papel de dona/administradora com o produto ligado. No máximo 10 chaves vivas por conta e produto. A chave (`secret`) volta UMA vez.",
      "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://staging.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://staging.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": "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://staging.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://staging.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, gratis?, facilitator, asset, asset_address, faucet, wallets, product, prices{contact_agent_usd,chat_month_usd,abuso_24h_usd}, chat{active,until}, limits{categories,groups_per_category,items_per_group} }",
      "url": "https://staging.gradetv.net/api/billing",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/contact",
      "auth": "none",
      "summary": "Contato e projetos de produtores: humano usa Turnstile; agente paga $0.10 por x402 ou crédito.",
      "grupo": "Cobrança",
      "desc": "Projetos de produtores: consulte GET /api/producers e use contact.message_template na mensagem. O primeiro envio de agente sai sem espera, após pagamento; depois o backoff é 60s dobrando até 1 hora (`Retry-After`). O valor paga somente o envio do contato, não o serviço de transmissão.",
      "corpo": {
        "name": {
          "tipo": "string",
          "desc": "Como chamar quem escreveu.",
          "obrigatorio": true
        },
        "email": {
          "tipo": "string",
          "desc": "Para onde responder.",
          "obrigatorio": true
        },
        "message": {
          "tipo": "string",
          "desc": "O que você quer dizer.",
          "obrigatorio": true
        },
        "form_ts": {
          "tipo": "int",
          "desc": "Início da composição, em milissegundos Unix: entre 2 segundos e 12 horas atrás, obrigatório também para agentes.",
          "obrigatorio": true
        },
        "cf_turnstile_response": {
          "tipo": "string",
          "desc": "Token Turnstile do formulário humano; ausente segue pelo pagamento de agente."
        },
        "tipo": {
          "tipo": "string",
          "desc": "Proposta: `patrocinio`, `parceria` ou `anuncio`. 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": "`ate_100`, `100_500`, `500_2000`, `2000_mais` ou `a_combinar`."
        },
        "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`."
        }
      },
      "body": {
        "name": "…",
        "email": "a@example.com",
        "message": "…"
      },
      "retorno": "Ok",
      "erros": {
        "400": "Campo obrigatório faltando.",
        "402": null,
        "403": "Verificação Turnstile inválida.",
        "429": "Backoff de agente: espere o `Retry-After`.",
        "502": "O provedor não aceitou o envio do e-mail; tente novamente mais tarde.",
        "503": "Envio indisponível por configuração de e-mail incompleta."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/contact -H \"X-PAYMENT: $PAGAMENTO\" -H 'content-type: application/json' -d '{\"name\":\"Agente\",\"email\":\"a@example.com\",\"message\":\"[Produtores] Quero apresentar meu projeto\"}'",
      "returns": "{ ok }",
      "url": "https://staging.gradetv.net/api/contact",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "POST",
      "path": "/api/visit",
      "auth": "none",
      "summary": "Ping da interface que incrementa a visita do dia. Agente não precisa chamar.",
      "grupo": "Cobrança",
      "desc": "Smoke não conta: `X-MM-Smoke`, User-Agent `mm-smoke` ou `smoke: true` no corpo entram como `counted: false`.",
      "corpo": {
        "smoke": {
          "tipo": "bool",
          "desc": "`true` marca a chamada como teste e ela não entra na contagem."
        }
      },
      "body": {
        "smoke": false
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Sempre `true`."
        },
        "counted": {
          "tipo": "bool",
          "desc": "Se a visita entrou na contagem 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 '{\"smoke\":true}'",
      "returns": "{ ok, counted, reason? }",
      "url": "https://staging.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."
        },
        "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."
        }
      },
      "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://staging.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://staging.gradetv.net/api/pagamento/aberto",
      "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 (`UTC`).",
          "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": "Visitantes únicos na borda em 7 dias.",
          "opcional": true
        },
        "pessoas": {
          "tipo": "object",
          "desc": "GA4 quando há: usuários, sessões, países, aparelhos e quem chegou de IA.",
          "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": "Impressões e cliques por patrocinador, quando houver.",
          "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://staging.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://staging.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://staging.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": "O cursor de erro resolvido por produto (`borda`, `cli`) — só com o token do operador.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` — a classe operador.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "_texto": "JSON: `{ [produto]: { borda?: 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, cli?: ISO } }`; vazio é `{}`.",
      "url": "https://staging.gradetv.net/api/vitrine/cursores",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/partners",
      "auth": "none",
      "grupo": "Parceria",
      "summary": "Parceria, patrocínio e anúncio: os espaços do produto com preço sugerido, os números públicos ao lado e como propor.",
      "desc": "Informação sob consulta, sem ativação: espaços do catálogo da casa com preço em USD por 30 dias (90 e 365 dias com desconto), patrocinadores em vigor, recorte de `/api/vitrine`, carteira da casa (USDC na Base) e o caminho de contato — depósito, PIX ou fatura são combinados na resposta. Cache de 1 hora.",
      "retorno": {
        "status": {
          "tipo": "string",
          "desc": "`sob_consulta`: informação e proposta, sem ativação nem cobrança."
        },
        "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": "Os espaços do produto: `id`, `nome`, `onde`, `formato`, `exclusivo`, `medicao`, `price_usd_30d` (sugestão; `null` é sob consulta), `exposure[{ dias, price_usd }]` para 30, 90 e 365 dias, `disponivel`."
        },
        "house_bundle": {
          "tipo": "object",
          "desc": "O pacote da casa: rodapé e menção para agentes nos dez produtos, com desconto."
        },
        "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": "Como pagar: `rede`, `chain_id`, `ativo`, `pay_to`, `eip681` (a carteira da casa, quando declarada), `alternativas` e a `nota` — depósito, PIX ou fatura pela resposta."
        },
        "contact": {
          "tipo": "object",
          "desc": "`email`, `form_url`, `api_url` (`POST /api/contact` onde há handler), `campos` (os obrigatórios), `campos_proposta` (os opcionais da proposta, com os valores aceitos de cada um), `price_agent_usd`, `message_template`, `instructions`."
        },
        "politica": {
          "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, house_bundle, parcerias, current_sponsors, stats, payment, contact, politica, _links }",
      "url": "https://staging.gradetv.net/api/partners",
      "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.",
      "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://staging.gradetv.net/api/metrics",
      "auth_detail": "Público, sem credencial."
    },
    {
      "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://staging.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://staging.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://staging.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://staging.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 64 KB 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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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://staging.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": "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",
      "query": {
        "usd": {
          "tipo": "int",
          "desc": "Pacote: 1, 5, 10 ou 25 dólares.",
          "obrigatorio": true
        }
      },
      "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."
        },
        "saldo_em": {
          "tipo": "string",
          "desc": "Onde consultar saldo e extrato."
        }
      },
      "erros": {
        "400": "Pacote fora da lista (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, saldo_em }",
      "url": "https://staging.gradetv.net/api/credito",
      "auth_detail": "Público, sem credencial."
    },
    {
      "method": "GET",
      "path": "/api/credito",
      "auth": "credito",
      "summary": "Saldo e extrato do crédito — as últimas movimentações, sem devolver o token.",
      "grupo": "Crédito",
      "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."
        }
      },
      "erros": {
        "401": "Sem token ou token desconhecido."
      },
      "exemplo": "curl -s $ORIGIN/api/credito -H 'Authorization: Bearer cred_…'",
      "returns": "{ saldo_micros, saldo_usd, criado_em, movimentos }",
      "url": "https://staging.gradetv.net/api/credito",
      "auth_detail": "Token de crédito em `Authorization: Bearer cred_…` (ou header `X-Credito`). Não é conta: é portador de saldo."
    },
    {
      "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?,trial?}, pricing, billing, api_index }",
      "url": "https://staging.gradetv.net/api/pricing",
      "auth_detail": "Público, sem credencial."
    }
  ],
  "quota": {
    "free": [
      {
        "o_que": "catálogo, busca e facets (`GET /api/channels`)",
        "limite": "sem cota",
        "janela": null
      },
      {
        "o_que": "galeria pessoal, pastas e feeds M3U/JSON/XSPF",
        "limite": "sem cota por convidado",
        "janela": null
      },
      {
        "o_que": "ler chat, comentários e saúde de canal",
        "limite": "sem cota",
        "janela": null
      }
    ],
    "paid": [
      {
        "o_que": "escrever no chat (30 dias)",
        "price_usd": 0.1
      },
      {
        "o_que": "contato de agente",
        "price_usd": 0.1
      }
    ],
    "how_to_pay": "Rota paga responde **402** com `accepts[]` (x402, USDC na Base). Pague e repita a mesma chamada com `X-PAYMENT`.",
    "live": "https://staging.gradetv.net/api/billing"
  },
  "mcp": {
    "endpoint": "https://staging.gradetv.net/mcp",
    "transport": "streamable-http",
    "tools": 40,
    "note": "Pluga direto no cliente MCP; sem instalar nada. Tools = as operações abaixo."
  },
  "quickstart": [
    "POST https://staging.gradetv.net/api/guest → ipt_…",
    "GET https://staging.gradetv.net/api/channels?country=BR&playable=1",
    "GET https://staging.gradetv.net/api/library com X-Guest-Token: ipt_…"
  ]
}