# Operations Videos — Master Implementation Guide (LLM-Ready)
> Hospedagem e player de vídeo para a página de quem vende ou ensina. A barra
> é sempre o tempo real; o produto mede quanto de cada vídeo foi assistido e,
> **entre as pessoas que se identificaram**, quem assistiu e até onde — e
> manda isso por webhook assinado para onde a conta quiser.
>
> Este arquivo é a documentação para agentes. Cole a URL no seu agente
> (Claude Code, Cursor, Windsurf, Codex) e peça a integração. **Leia a seção 9
> antes de prometer qualquer coisa**: ela diz o que o produto não faz.
Worker (API, player, MCP): https://videos.worker.myoperations.click
Painel: https://videos.dashboard.myoperations.click
Servidor MCP: https://videos.worker.myoperations.click/mcp — ver a seção 8.
O mapa da linha (Analytics, Videos, login): https://myoperations.click/llms.txt
## 0. Estado e credenciais — leia antes de tudo
- **No ar desde 02/10/2026.** `GET https://videos.worker.myoperations.click/`
responde `{"service":"operations-videos",…}`.
- **Login:** o Auth da linha (`https://auth.worker.myoperations.click`,
aplicação Operations), com código por e-mail. É o mesmo login do Analytics:
mesmo login, autorização por produto. Passo humano obrigatório: a pessoa lê
um código de 8 caracteres no e-mail e o informa. Não tente contornar.
- **Chave da conta:** `vk_…`, criada no painel em **Integrações e eventos**.
Vale para a API (`Authorization: Bearer vk_…`) e para assinar a identidade
de quem está logado na página (seção 3). **Fica só no servidor**: nunca no
HTML, nunca num repositório.
- **MCP:** OAuth, com o mesmo login (seção 8). Não usa a `vk_`.
- **Preço:** não há preço público e nada é cobrado.
Não cite valor, plano, franquia em minutos nem "grátis".
Se perguntarem: "Ainda não há planos, e nada é cobrado."
Sem credencial de verdade, **pare e peça**. Não gere código com `vk_xxx` de
placeholder e diga que terminou.
## 1. Modelo mental
- **Vídeo** — tem `id` (UUID), `code` público (`v_XXXX`), título e tipo
(`vsl`, `demo`, `lesson`, `other`). `vsl` e `demo` começam mudos e sozinhos;
`lesson` espera o play e retoma de onde parou.
- **Documento do vídeo** (`videos.config/1`) — capítulos, player, ações e
domínios permitidos num documento só, com versões. O painel (ou o agente)
edita um **rascunho**; **publicar** cria a versão seguinte e nunca
sobrescreve; uma versão antiga reabre como rascunho. Publicar por cima de
outra aba responde conflito.
- **Sessão** — quem abriu o player. Só ganha dono por quatro portas (seção 3).
- **Minutos, não plays** — três medidas separadas:
- **entregue**: todo tempo tocado, com ou sem som;
- **assistido**: só depois do clique para ouvir; salto não conta;
- **com dono**: assistido por pessoa identificada.
O produto não conta plays.
## 2. Enviar e incorporar
**Enviar:**
- Do computador: envio retomável (tus), direto do navegador ao fornecedor;
MP4, MOV, MKV ou WebM até 20 GB.
- Por link: um `https://` para o arquivo. Links de YouTube, Vimeo, Instagram,
TikTok e Facebook são recusados no servidor.
- Pela API ou pelo MCP: a criação devolve um link de envio tus válido por
1 hora. **O arquivo nunca passa pelo chat.**
```
POST https://videos.worker.myoperations.click/api/videos
Authorization: Bearer vk_…
{ "title": "Demo v4", "kind": "demo" }
→ 201 { "video": { "id": "…", "code": "v_8KQ2", "status": "uploading", … },
"upload": { "protocol": "tus", "url": "…", "headers": { … }, "expiresIn": 3600 } }
```
O vídeo toca quando a primeira qualidade fica pronta, e as outras entram
sozinhas; o webhook `video.ready` avisa. Substituir o arquivo
(`POST /api/videos/:id/replace`) mantém código, link e dados.
**Incorporar** — um script onde o vídeo deve aparecer. O código aponta para o
vídeo, não para uma versão: publicar de novo não exige colar nada.
```html
```
O player é um Web Component com Shadow DOM, em JavaScript puro: não herda nem
quebra o CSS da página. Autoplay mudo com "Clique para ouvir" (volta a 0:00
com som), retomada guardada no navegador de quem assiste, velocidade, tela
cheia e atalhos de teclado. A barra é sempre o tempo real
(`progress_bar: "real_time"` é constante no esquema).
**Só toca nos domínios permitidos da conta** (`access.allowed_domains`): eles
são conferidos antes de o endereço do streaming ser assinado, e a assinatura
vale 6 horas. Fora deles, o player responde "Este vídeo não toca neste
endereço."
## 3. Quem assistiu — as quatro portas
Uma sessão só ganha dono por uma destas portas:
| Porta | Como | `identifiedBy` |
|---|---|---|
| Link com identidade | `?vw=` na URL da página: código opaco, com validade (7, 30 ou 90 dias), gerado por lista de e-mails; só o hash fica guardado; vencido, abre anônimo | `identity_link` |
| E-mail no player | a ação `email_gate` (sempre pulável) e a pessoa responde | `email_gate` |
| Login provado pelo servidor da página | `data-viewer` com um token assinado pela `vk_` ou com o JWT da aplicação no Auth | `signed_login` |
| Compra informada pela API | `POST /api/conversions` com a sessão que o botão levou ao checkout | — |
**`data-email` sem assinatura é ignorado**: qualquer um escreveria.
O token de `data-viewer` é `v1.." com a vk_>`. Em Node:
```js
import { createHmac } from 'node:crypto'
const key = process.env.VIDEOS_KEY // vk_…, só no servidor
const b64 = text => Buffer.from(text).toString('base64url')
export function viewerToken({ email, name }) {
const payload = b64(JSON.stringify({ email, name, exp: Math.floor(Date.now() / 1000) + 3600 }))
return `v1.${payload}.${createHmac('sha256', key).update(`v1.${payload}`).digest('base64url')}`
}
```
**Anônimo continua anônimo:** sem fingerprint, sem IP gravado. Identificar liga
a sessão daquele momento em diante; visitas anônimas antigas não são ligadas a
quem se identificou depois. Visitante frio de anúncio vira contagem.
## 4. Medir
- **Retenção** (`GET /api/videos/:id/retention?days=7|30`): de quem escolheu
assistir (com som), quantos ainda estavam lá a cada trecho, para todo o
público e só para as pessoas conhecidas; maior queda (trecho e capítulo),
quanto se assiste em média, quantos chegaram à metade, clicaram no botão e
terminaram. Marcos de 25, 50, 75 e 100%.
- **Quem assistiu** (`GET /api/viewers`, `GET /api/videos/:id/viewers`): só
pessoas identificadas, com `filter=all|cta|half_no_cta` ("passaram da metade
e não clicaram").
- **Apagar uma pessoa** (`DELETE /api/viewers/:id`) apaga também as sessões
dela.
## 5. O documento `videos.config/1`
```
player autoplay (muted|off) · resume · speeds · accent (#RRGGBB)
poster_at_s · progress_bar ("real_time", constante)
chapters [{ id, start_s, title }] o primeiro em 0; até 50
actions [{ id, trigger, effect, audience, once_per_session: true }] até 20
access allowed_domains (até 20) · signed_urls · download
```
- `trigger.on`: `time` (`at_s`) · `chapter_start` (`chapter`) · `progress`
(`percent`: 10, 25, 50, 75 ou 90) · `first_pause` · `end`.
- `effect.type`: `button` (`text`, `url`, `placement`: `below_video` |
`over_video`) · `reveal` (`selector`) · `card` (`title`, `body`, `link`) ·
`email_gate` (`prompt`, `skippable: true`) · `emit` (`event:
"video.reached"`, `name`).
- `audience`: `all` · `known` · `anon`. Toda ação dispara uma vez por sessão.
- Referência sempre por `id`, nunca por posição.
**Verificações antes de publicar:** capítulo em 0:00, capítulo de 10 s ou mais,
ação apontando para capítulo que existe, link `https://`, domínio válido. Com
erro, o worker recusa publicar.
**Mostrar um bloco no minuto certo:** marque a seção com a classe
`videos-oculto`; o player esconde, e uma ação `reveal` tira a classe no
momento escolhido.
```html
O preço e o botão de compra.
```
## 6. Rotas
Base: `https://videos.worker.myoperations.click`, com `Authorization: Bearer
vk_…` (ou a sessão do painel). Coleções respondem `{items, page}`; erros,
`{error: {code, message}}`.
```
GET /api/videos os vídeos, com os números de 30 dias
POST /api/videos cria um vídeo e devolve o envio (tus, 1 hora)
POST /api/videos/import cria por link https:// para o arquivo
GET /api/videos/:id o vídeo, a versão publicada e o rascunho
PATCH /api/videos/:id título e tipo (fora do rascunho)
DELETE /api/videos/:id apaga o vídeo — só com pedido explícito de uma pessoa
PUT /api/videos/:id/draft salva o rascunho do videos.config/1
POST /api/videos/:id/publish publica o rascunho
GET /api/videos/:id/versions as versões
POST /api/videos/:id/versions/:n/reopen reabre uma versão como rascunho
POST /api/videos/:id/replace substitui o arquivo, mantendo código e dados
POST /api/videos/:id/thumbnail miniatura (JPEG)
GET /api/videos/:id/retention?days=30
GET /api/videos/:id/viewers quem assistiu, só pessoas conhecidas
GET /api/viewers?filter=half_no_cta passaram da metade e não clicaram
DELETE /api/viewers/:id apaga a pessoa e as sessões dela
POST /api/identity-links um link com identidade por e-mail
POST /api/conversions uma venda liga a sessão (vs=) a quem comprou
GET /api/webhooks os endpoints e os eventos que cada um assina
POST /api/webhooks { url, description?, events?, secret? }
PATCH /api/webhooks/:id { url?, description?, events?, isActive? }
POST /api/webhooks/:id/secret troca o segredo (gerado, ou o seu)
POST /api/webhooks/:id/test um evento de teste assinado, agora
DELETE /api/webhooks/:id
GET /api/deliveries?endpointId= as últimas entregas
POST /api/deliveries/:id/retry reenvia uma entrega que falhou
GET /api/keys · POST /api/keys a chave vk_ da conta
GET /api/plan o uso do mês
```
**Vendas:** o botão leva `vs=` com a sessão no link. Qualquer checkout guarda
esse valor e, quando a venda confirmar, o seu servidor chama:
```
POST /api/conversions
Authorization: Bearer vk_…
{ "session": "s_Q2x8Rk91LmFb", "email": "…", "name": "…",
"externalId": "pedido-1042", "value": 197, "currency": "BRL" }
→ 200 { "session": "s_Q2x8Rk91LmFb", "video": "v_7PX4", "becameKnown": true, "converted": true }
```
Nenhuma plataforma de venda tem integração própria; todas usam esta chamada.
## 7. Webhooks
Até 20 endpoints por conta, cada um com os eventos que assina (sem `events`,
assina todos). Padrão **Standard Webhooks** com o prefixo `x-ccm`:
```
x-ccm-id: s_Q2x8Rk91LmFb.video.watched.75
x-ccm-timestamp: 1790265731
x-ccm-signature: v1,
```
A chave do HMAC é o base64 depois de `whsec_`. Recuse timestamp com mais de
5 minutos e ignore um `x-ccm-id` já visto. O segredo é gerado (mostrado uma
vez) ou o seu — uma fonte do People já traz o dela.
Corpo: `{ type, timestamp, data: { person, channel, properties } }`. Evento de
pessoa só existe com pessoa conhecida.
| Evento | Quando |
|---|---|
| `video.watched` | a pessoa passou de 25, 50, 75 e 100% — um evento por marco |
| `video.cta_clicked` | clicou no botão do vídeo, com a UTM da página |
| `video.reached` | uma ação `emit` disparou, com o nome e o minuto |
| `video.converted` | uma compra chegou por `/api/conversions` |
| `person.identified` | a sessão ganhou dono |
| `video.ready` | a conversão terminou e o vídeo toca (sem pessoa) |
Se o destino falhar: nova tentativa em 1 min, 5 min, 30 min, 2 h e 12 h.
**Para a ficha no People** (Customers): o People é um endpoint como qualquer
outro — a URL e o segredo de uma fonte criada no People, assinando só os
eventos com pessoa. Não é automático.
## 8. Servidor MCP
- **Endereço:** `https://videos.worker.myoperations.click/mcp` (Streamable
HTTP), para a conta inteira — a conta vem do token, nunca de um argumento.
- **OAuth 2.1:** metadados em
`https://videos.worker.myoperations.click/.well-known/oauth-protected-resource`.
O token só vale para este servidor (audiência conferida). Escopos:
`auth:read` lista as quatro de leitura; `auth:write` acrescenta as três de
escrita.
- **Limite:** 120 chamadas por minuto por conta.
| Tool | Escopo | O que faz |
|---|---|---|
| `videos_list` | leitura | os vídeos, com os números de 30 dias; filtra por tipo ou título |
| `videos_get_retention` | leitura | a curva de retenção (7 ou 30 dias), maior queda, metade, botão, fim |
| `videos_list_viewers` | leitura | as pessoas conhecidas dos últimos 90 dias; `filter=half_no_cta` |
| `videos_usage` | leitura | o uso do mês (minutos entregues, guardados, vídeos) |
| `videos_create_upload` | escrita | cria um vídeo e devolve o link de envio tus (1 hora) |
| `videos_update_player` | escrita | altera **só o rascunho** e devolve as verificações |
| `videos_identity_links` | escrita | gera um link com identidade por e-mail |
As de escrita levam `destructiveHint`: um cliente sério pede confirmação à
pessoa. **Publicar e apagar não são ferramentas** — são de uma pessoa, no
painel. O agente escreve no rascunho. Quem publica é você.
## 9. Escopo — o que este produto NÃO faz
- **Teste A/B**, conversa com IA sobre os vídeos.
- **Legendas, marca d'água, DRM**, proteção contra download como recurso.
- **Domínio próprio** no player ou na entrega: o endereço do streaming leva o
nome do fornecedor.
- **Contagem de plays.** O produto conta minutos.
- **Identificar todo espectador.** Só quem se identificou por uma das quatro
portas; o resto é contagem.
- **Integração pronta com plataformas de venda ou com o Payments.** A venda
entra pela chamada `POST /api/conversions`, que alguém precisa fazer.
- **Área de membros, live, tutor por IA, dublagem.**
- **Cobrança.** Não há plano publicado, e nada é cobrado.
- **Armazenamento no Brasil.** Um fornecedor de vídeo guarda, converte e
entrega; o armazenamento principal fica na **Alemanha, com réplica em São
Paulo**. O player, a medição, o documento e as integrações são do Videos.
Não afirme "vídeo guardado no Brasil".
## 10. Checklist para agentes de IA
1. **Peça a `vk_`** para a API; para o MCP, o login por código no e-mail.
2. **Cadastre o domínio da página** em `access.allowed_domains` antes de
testar: fora dele, o vídeo não toca.
3. **Nunca ponha a `vk_` no HTML.** O token de `data-viewer` é feito no
servidor.
4. **Não mande o arquivo pelo chat.** Entregue o link de envio tus.
5. **Escreva no rascunho e peça a uma pessoa para publicar.**
6. **"Quem assistiu" vem sempre com "entre quem se identificou".**
7. **Não invente rota nem tool.** A seção 6 lista as rotas de integração; a 8, as tools.