# API — integrar uma aplicação ao Cakto TV Este documento é para quem vai **usar** o Cakto TV a partir de outra aplicação: subir conteúdo, vincular TVs e escolher o que cada uma mostra. Tudo é HTTP com JSON. Não precisa de biblioteca. ``` Base: https://SERVIDOR/api/v1 Auth: Authorization: Bearer ``` ## Conceitos | Termo | O que é | |---|---| | **Conta** | A sua aplicação. Tem uma chave de API (`ctv_...`), as próprias TVs e o próprio conteúdo. Uma conta nunca enxerga nada de outra. | | **TV** | Uma tela LG (ou qualquer navegador) que rodou o app e foi autorizada pela sua conta. Tem id `dev_...`. | | **Tela** | O que uma TV está mostrando. Pode ser uma tela embutida, um arquivo que você enviou ou uma URL. | | **Código de pareamento** | 6 caracteres que a TV exibe ao ligar. Vale 5 minutos, serve uma vez. Não dá acesso a nada; é só o pedido de autorização. | A chave da conta fica no console (`/app` → **API**), depois de criar a conta em `/cadastro`. Guarde-a no servidor da sua aplicação. **Nunca a coloque em frontend, app mobile ou na TV** — a TV recebe uma credencial própria, que só abre os recursos dela. Se vazar, gere outra no console; a antiga para na hora. O console usa exatamente esta API, autenticado por cookie de sessão em vez de chave. Tudo que ele faz, sua aplicação pode fazer. ## Fluxo em 4 chamadas ``` 1. A TV liga e mostra: "Código: 7K8P2X" 2. Sua aplicação: POST /tvs/parear {codigo:"7K8P2X", nome:"Recepção"} → tv.id = dev_ab12 3. Sua aplicação: PUT /conteudo/promo.jpg (bytes da imagem) → url = /conteudo/cta_x/promo.jpg 4. Sua aplicação: POST /tvs/tela {ids:["dev_ab12"], tela:"/conteudo/cta_x/promo.jpg"} ``` A TV troca na hora. Ela mantém uma conexão aberta com o servidor e recebe cada mudança sem ninguém encostar no controle remoto. ## Referência Toda resposta de erro tem o formato `{ "erro": "mensagem legível" }`. | Código | Quando | |---|---| | 400 | corpo inválido ou faltando campo | | 401 | chave de API inválida | | 403 | credencial de TV usada onde só chave de conta vale | | 404 | TV, arquivo ou código de pareamento não encontrado (ou de outra conta) | | 409 | operação fora de ordem (ex.: apagar TV antes de revogar) | ### Conta **`GET /eu`** — confirma a chave e devolve a conta. ```json { "id": "cta_aa6fa7b6", "nome": "Minha aplicação", "criadoEm": "2026-09-14T17:38:00.000Z" } ``` ### Links salvos Uma URL que aparece na biblioteca com nome, para ser escolhida sem digitar de novo. Não é obrigatório salvar: `POST /tvs/tela` aceita qualquer URL direto. **`GET /links`** · **`POST /links`** `{ "url": "https://…", "nome": "opcional", "login": false }` · **`DELETE /links/:id`** #### Sites com login Um painel que pede senha não abre na TV: ela não tem os cookies. Com `"login": true`, o servidor abre esse link num navegador próprio, com perfil persistente por link. Você faz o login uma vez (pelo console, em **Entrar no site**, ou pela API abaixo) e a TV passa a receber **capturas** da página já autenticada, renovadas a cada 5 segundos. Os cookies nunca saem do servidor. Na lista de telas o link aparece como `"/captura/"`; é esse valor que vai em `POST /tvs/tela`. **`GET /links/:id/captura.jpg`** — captura atual (1920×1080). `?fresca=1` força uma nova. **`POST /links/:id/navegador`** — age no navegador do servidor. Um objeto por chamada: ```json { "acao": "clique", "x": 960, "y": 540 } { "acao": "texto", "texto": "usuario@empresa.com" } { "acao": "tecla", "tecla": "Enter" } { "acao": "rolar", "dy": 400 } { "acao": "ir", "url": "https://…" } { "acao": "recarregar" } { "acao": "salvar" } ``` Responde `{ "ok": true, "url": "" }`. `salvar` fecha o navegador gravando os cookies em disco; chame ao terminar o login. Apagar o link apaga o perfil (e os cookies) junto. Limite prático: cada link com login aberto consome um Chromium (~250 MB de RAM). O navegador fecha sozinho após 10 minutos sem nenhuma TV pedir captura e reabre na próxima. ### TVs **`POST /tvs/parear`** — autoriza a TV que está mostrando o código. É aqui que a TV passa a existir na sua conta e recebe a credencial dela. ```json { "codigo": "7K8P2X", "nome": "Recepção", "area": "Térreo" } ``` `nome` é obrigatório, `area` é livre. Resposta `201`: ```json { "ok": true, "tv": { "id": "dev_2be26def", "nome": "Recepção", "area": "Térreo", "tela": "dashboard", "status": "ativo", "online": false, "ultimoContato": null, "criadoEm": "2026-09-14T17:38:42.891Z" } } ``` `404` se o código não existir, tiver expirado ou já tiver sido usado. A TV gera outro sozinha; use o que estiver na tela dela agora. **`GET /tvs`** — todas as TVs da conta e as telas disponíveis. ```json { "tvs": [ { "id": "dev_2be26def", "nome": "Recepção", "tela": "dashboard", "status": "ativo", "online": true, "ultimoContato": "…", "…": "…" } ], "telas": [ { "tela": "dashboard", "nome": "Dashboard de vendas", "tipo": "embutida" }, { "tela": "/conteudo/cta_aa6fa7b6/promo.jpg", "nome": "promo.jpg", "tipo": "conteudo" } ] } ``` `online` é verdadeiro quando a TV está com a conexão de eventos aberta neste momento. `ultimoContato` é a última vez que ela falou com o servidor. **`GET /tvs/:id`** — uma TV. **`POST /tvs/tela`** — define o que uma ou mais TVs mostram. ```json { "ids": ["dev_2be26def", "dev_9f01c2aa"], "tela": "/conteudo/cta_aa6fa7b6/promo.jpg" } ``` `tela` aceita três formas: | Forma | Exemplo | O que a TV faz | |---|---|---| | slug embutido | `"dashboard"`, `"meta"`, `"aviso"` | abre a tela pronta do sistema, alimentada pelo `/dashboard` | | arquivo enviado | `"/conteudo/cta_x/promo.jpg"` | imagem: tela cheia sobre fundo escuro. HTML: tela cheia | | URL externa | `"https://exemplo.com/painel"` | abre a página em tela cheia | | vídeo do YouTube | `"https://www.youtube.com/watch?v=ID"` | toca em tela cheia, sem som, em loop (link normal, `youtu.be` ou `shorts`) | Uma URL externa só funciona se o site permitir ser aberto dentro de outra página (sem `X-Frame-Options: DENY`). YouTube é tratado à parte, por isso funciona. Resposta: `{ "ok": true, "trocadas": ["dev_2be26def"] }`. Ids de outra conta, inexistentes ou revogados são ignorados em silêncio e não aparecem em `trocadas`. `400` se `tela` não for válida para a sua conta (arquivo de outra conta, por exemplo). **`POST /tvs/:id/revogar`** — corta o acesso da TV na hora. Ela apaga a credencial e volta para a tela de código. A TV continua na lista como `revogado`; para usá-la de novo, pareie o novo código que ela exibir. **`DELETE /tvs/:id`** — remove da lista. Só depois de revogada (`409` caso contrário). ### Conteúdo **`PUT /conteudo/`** — envia um arquivo. O corpo da requisição é o arquivo em si (não é multipart). Limite de 50 MB por arquivo. Enviar no mesmo caminho substitui. ```bash curl -T promo.jpg -H "Authorization: Bearer $CHAVE" https://SERVIDOR/api/v1/conteudo/promo.jpg ``` ```json { "ok": true, "caminho": "promo.jpg", "url": "/conteudo/cta_aa6fa7b6/promo.jpg", "tamanho": 184320 } ``` O `url` devolvido é o valor que você passa em `tela`. O caminho pode ter pastas: um site é um arquivo por chamada no mesmo prefixo, e a TV abre o `index.html`. ```bash curl -T index.html -H "Authorization: Bearer $CHAVE" https://SERVIDOR/api/v1/conteudo/promo/index.html curl -T style.css -H "Authorization: Bearer $CHAVE" https://SERVIDOR/api/v1/conteudo/promo/style.css curl -T logo.png -H "Authorization: Bearer $CHAVE" https://SERVIDOR/api/v1/conteudo/promo/logo.png # tela: "/conteudo/cta_aa6fa7b6/promo/index.html" ``` Dentro do HTML, referencie os outros arquivos por caminho relativo (`style.css`, `logo.png`). O HTML roda numa TV: não há mouse nem teclado, dimensione em `vw`, e evite qualquer coisa que dependa de clique. Os arquivos ficam acessíveis em `https://SERVIDOR/conteudo//` sem autenticação, para a TV conseguir carregá-los. Não envie nada que não possa ser visto por quem tiver a URL. **`GET /conteudo`** — lista o que a conta enviou. ```json { "arquivos": [ { "caminho": "promo.jpg", "url": "/conteudo/cta_aa6fa7b6/promo.jpg", "tamanho": 184320, "modificadoEm": "2026-09-14T17:40:00.000Z" } ] } ``` **`DELETE /conteudo/`** — apaga. TVs que estavam nesse arquivo continuam apontando para ele até você trocar a tela delas. ### Dashboard (telas embutidas) As telas `dashboard`, `meta` e `aviso` mostram estes quatro valores. Se você só usa conteúdo próprio, ignore esta seção. **`GET /dashboard`** e **`PUT /dashboard`** — campos opcionais, só os enviados mudam. ```json { "tpv": 184320.5, "vendas": 212, "meta": 250000, "mensagem": "Bom dia, time!" } ``` **`POST /celebrar`** — dispara a animação "META BATIDA" por 5 segundos, com o valor de `meta`. Corpo opcional `{ "ids": ["dev_…"] }` para limitar a algumas TVs; sem corpo, vai para todas. ### Eventos em tempo real **`GET /eventos?token=`** — Server-Sent Events. Como `EventSource` não envia header, aqui a chave vai na query. Use só em backend. | Evento | Quando | Dados | |---|---|---| | `tvs` | ao conectar e a cada mudança em TV, tela ou conteúdo | `{ tvs, telas, logs }` (mesmo formato de `GET /tvs`, mais os 12 últimos logs) | | `dashboard` | ao conectar e a cada `PUT /dashboard` | o dashboard | ### Log **`GET /logs`** — os últimos eventos da conta, mais recente primeiro. ```json { "logs": [ { "em": "…", "contaId": "cta_…", "acao": "tv.pareada", "detalhe": "Recepção (dev_…) autorizada via código 7K8P2X" } ] } ``` Ações: `tv.pareada`, `tv.revogada`, `tv.apagada`, `tela.trocada`, `conteudo.enviado`, `conteudo.apagado`, `pareamento.recusado`. ## Exemplo completo em Node ```js const BASE = "https://SERVIDOR/api/v1"; const H = { Authorization: `Bearer ${process.env.CAKTO_TV_KEY}` }; const json = (b) => ({ ...H, "content-type": "application/json" }); // 1. autoriza a TV que está mostrando o código const { tv } = await fetch(`${BASE}/tvs/parear`, { method: "POST", headers: json(), body: JSON.stringify({ codigo: "7K8P2X", nome: "Recepção" }), }).then((r) => r.json()); // 2. sobe uma imagem const { url } = await fetch(`${BASE}/conteudo/promo.jpg`, { method: "PUT", headers: H, body: await fs.promises.readFile("promo.jpg"), }).then((r) => r.json()); // 3. mostra na TV await fetch(`${BASE}/tvs/tela`, { method: "POST", headers: json(), body: JSON.stringify({ ids: [tv.id], tela: url }), }); ``` ## O que a TV faz sozinha Depois de pareada, a TV não precisa de nada: reconecta quando a rede ou o servidor caem, obedece a troca de tela ao vivo, mostra "sem conexão" enquanto não alcança o servidor e volta para a tela de código se for revogada. Se o app for fechado e reaberto, ela continua na conta, com a mesma tela. ### Conta e chave (só pelo console) `GET /chave`, `POST /chave/regenerar` e a troca de senha em `PUT /conta` só funcionam com a sessão do console: uma chave de API não lê nem troca a si mesma. `PUT /conta { "nome": "…" }` funciona pelos dois caminhos. ## Operação (quem administra o sistema, não quem integra) Contas normais nascem pelo cadastro em `/cadastro`. O operador ainda pode criar uma conta só com chave, sem login, para integrações internas: ```bash curl -X POST -H "Authorization: Bearer $OPERADOR_KEY" -H 'content-type: application/json' \ -d '{"nome":"Minha aplicação"}' https://SERVIDOR/admin/api/contas # → { "ok": true, "conta": { "id": "cta_…", "nome": "…", "apiKey": "ctv_…" } } (a chave aparece só aqui) ``` `GET /admin/api/contas` lista as contas (sem chaves) e `GET /admin/api/logs` mostra o log de todas.