{"openapi":"3.1.0","info":{"title":"API pública do Gamerfy","version":"1.0.0","summary":"Usuários, lives, replays, clipes, jogos e webhooks do Gamerfy, no molde da Twitch.","description":"Consulte usuários, lives, replays, clipes e jogos do Gamerfy e receba webhooks\nquando algo acontece. A API segue o molde da Helix, da Twitch: um cliente\nescrito para a Twitch precisa mudar pouco além do endereço.\n\nEsta página é a referência completa. À esquerda ficam o guia e cada rota; à\ndireita, exemplos prontos para copiar. O botão **Testar** faz a chamada de\nverdade daqui mesmo.\n\n## Primeiros passos\n\n### 1. Crie um app\n\n1. No Gamerfy, abra **Configurações → Desenvolvedor**.\n2. Clique em **Criar app** e dê um nome (até 60 caracteres).\n3. Copie o **client secret** na hora. Ele aparece **uma única vez** (o\n   Gamerfy guarda só o SHA-256). Se perder, use **Gerar novo segredo**: o\n   anterior deixa de valer e todos os tokens emitidos com ele são apagados.\n4. O **client id** fica visível na lista e pode ser copiado quando quiser.\n\n| Credencial | Formato | Onde guardar |\n|---|---|---|\n| `client_id` | 30 caracteres `[a-z0-9]` | público; pode ir em arquivo de configuração |\n| `client_secret` | `gfs_` + 40 caracteres `[A-Za-z0-9]` | segredo; só no servidor de quem integra |\n| token de app | `gfa_` + 40 caracteres `[A-Za-z0-9]` | segredo; expira em 60 dias |\n\nCada conta pode ter até **10 apps**. Apagar o app apaga o segredo e todos os\ntokens.\n\n### 2. Peça o token\n\n```bash\ncurl -X POST https://api.gamerfy.gg/oauth2/token \\\n  -d 'client_id=abc123…' \\\n  -d 'client_secret=gfs_…' \\\n  -d 'grant_type=client_credentials'\n```\n\nA resposta traz `access_token` e `expires_in` (em segundos, 60 dias). O corpo\ntambém pode ser JSON. Guarde o token e reutilize até expirar: pedir um novo a\ncada chamada é desperdício e conta no limite.\n\n### 3. Faça a primeira chamada\n\n```bash\ncurl 'https://api.gamerfy.gg/v1/streams?game_id=pubg' \\\n  -H 'Authorization: Bearer gfa_…'\n```\n\nPara testar aqui na página, clique em **Testar** numa rota e cole o token em\n**Autenticação**.\n\n### Importar no Postman ou no Insomnia\n\nA especificação OpenAPI 3.1 está em `https://api.gamerfy.gg/openapi.json`. No\nPostman: **Import → Link** e cole o endereço; a coleção sai com todas as rotas\ne exemplos.\n\n## Formato das respostas\n\n- Todas as rotas `/v1/*` exigem `Authorization: Bearer gfa_…`.\n- Listas vêm em `{ \"data\": [ … ] }`. As rotas paginadas (`/v1/streams`,\n  `/v1/videos`, `/v1/clips`) trazem também `pagination`.\n- Campos em `snake_case`; datas em ISO 8601 UTC com milissegundos\n  (`2026-09-08T21:30:00.123Z`).\n- Ids são strings (snowflakes de 64 bits: não cabem em `Number` do\n  JavaScript sem perda).\n- Parâmetros repetíveis aceitam `?id=1&id=2`, até 100 valores por chamada.\n- Um id ou login que não existe simplesmente não aparece em `data`; não é erro.\n- Qualquer parâmetro recusado é `400 invalid_query`, com `details` listando\n  `{ path, message }` por campo.\n\n## Paginação\n\n`/v1/streams`, `/v1/videos` e `/v1/clips` paginam por cursor. `first` é o\ntamanho da página (1 a 100; padrão 20). Enquanto houver mais, a resposta traz\n`pagination.cursor`: repita a chamada com os **mesmos filtros** e\n`after=<cursor>`.\n\n```bash\ncurl 'https://api.gamerfy.gg/v1/videos?game_id=pubg&first=100&after=eyJzdGFydGVkX2F0Ijoi…' \\\n  -H 'Authorization: Bearer gfa_…'\n```\n\n- O cursor é opaco: não monte nem edite. Um cursor corrompido é\n  `400 invalid_cursor`.\n- Use cada cursor só na rota e com os filtros que o emitiram.\n- Uma página que veio **cheia** sempre traz cursor, mesmo quando por acaso é a\n  última: a chamada seguinte devolve `data: []` e `pagination: {}`. Pare quando\n  `pagination.cursor` faltar ou `data` vier vazio.\n- A lista é sempre do mais novo para o mais velho; não existe `before`.\n\n## Ligar um evento da partida ao replay\n\nÉ o caso do pubg.report: a telemetria diz que uma morte aconteceu em\n`event_time_utc`, e a API diz que o replay começou em `video.created_at`. O\ninstante no vídeo é a diferença, sem nenhuma correção:\n\n```\noffset_s = (event_time_utc − video.created_at) / 1000\nurl      = video.url + \"?t=\" + formatar(offset_s)     # 1h2m3s, ou só os segundos\n```\n\n**Por que sem recuo.** `created_at` não é a hora em que a live foi anunciada.\nÉ o `EXT-X-PROGRAM-DATE-TIME` do primeiro segmento gravado: o relógio do\nservidor de mídia no primeiro quadro, com milissegundos. Na Twitch o\n`created_at` do VOD chega a ficar dezenas de segundos antes do primeiro quadro,\ne por isso o pubg.report desconta 10 s. Aqui o desconto seria erro.\n\nO que ainda entra na conta é a latência do encoder (OBS ou o app) até o\nservidor, normalmente abaixo de 2 s. Eventos antes de `created_at` ou depois de\n`created_at + duration_seconds` não têm imagem: o replay pode ter começado\ndepois da partida ou ter sido truncado.\n\nA página `https://gamerfy.gg/videos/{id}?t=…` aceita `1h2m3s`, `2m3s`, `45s`\ne `45`. Um `t` inválido abre o vídeo do começo.\n\n**Clipes.** O `vod_offset` de um clipe é contado a partir do mesmo\n`video.created_at`, então é comparável ao `offset_s` acima. O replay abre no\nponto do clipe com `https://gamerfy.gg/videos/{video_id}?t={vod_offset}s`.\n\n## Autorização de apps\n\nO token de app (`gfa_…`) lê o que é público. Para ler a live **de um streamer** —\no chat, quem seguiu, quem mandou presente — o seu app pede a autorização dele,\nno molde da Twitch: código de uso único com PKCE (OAuth 2, RFC 6749 e 7636). Ele\nvê em palavras o que o app vai poder, aceita ou recusa, e pode tirar o acesso a\nqualquer momento em **Configurações → Conexões**.\n\n### 1. Cadastre os endereços de volta\n\nEm **Configurações → Desenvolvedor → Autorização**, cadastre até **10**\nendereços para onde o streamer volta depois de aceitar. Cada um é `https://`\nexato, ou o do próprio computador — `http://127.0.0.1:<porta>/…` e\n`http://localhost:<porta>/…` com **qualquer porta** (RFC 8252), para apps de\ndesktop como o Streamer.bot.\n\nNo mesmo lugar, **Página do app**: o endereço onde o streamer **usa** o seu app — o painel, as configurações. É\nopcional, e segue a mesma regra dos endereços de volta (`https://`, ou `http://localhost:<porta>` para um app que\nroda na máquina dele). Com ela, a linha do seu app em **Configurações → Conexões** vira um botão que abre a página.\n\n### 2. Mande o streamer para a tela de autorizar\n\n```\nhttps://gamerfy.gg/autorizar?response_type=code&client_id=…&redirect_uri=…&scope=chat:read%20gifts:read&state=…&code_challenge=…&code_challenge_method=S256\n```\n\n- `scope`: as permissões, separadas por espaço (a tabela abaixo).\n- `state`: um valor seu, aleatório; ele volta igual — confira para recusar uma\n  volta que você não pediu.\n- `code_challenge`: `base64url(SHA-256(code_verifier))`. **PKCE com `S256` é\n  obrigatório** para todo app; `plain` é recusado. O `code_verifier` tem de 43 a\n  128 caracteres `[A-Za-z0-9-._~]` e fica guardado com você até a volta.\n\nSem sessão no Gamerfy, o streamer entra primeiro e volta para a tela. Aceitou:\nele vai para `redirect_uri?code=…&state=…`. Recusou:\n`redirect_uri?error=access_denied&state=…`. O código vale **60 segundos** e uma\nvez só.\n\n### 3. Troque o código pelas chaves\n\n```bash\ncurl -X POST https://api.gamerfy.gg/oauth2/token \\\n  -d 'grant_type=authorization_code' \\\n  -d 'client_id=abc123…' \\\n  -d 'code=…' \\\n  -d 'redirect_uri=http://127.0.0.1:8080/callback' \\\n  -d 'code_verifier=…'\n```\n\n```json\n{ \"access_token\": \"gfp_…\", \"refresh_token\": \"gfr_…\", \"token_type\": \"bearer\", \"expires_in\": 14400, \"scope\": \"chat:read gifts:read\" }\n```\n\nO mesmo caminho em JavaScript, num app de desktop:\n\n```js\nimport { createHash, randomBytes } from 'node:crypto';\n\nconst CLIENT_ID = 'abc123…';\nconst REDIRECT_URI = 'http://127.0.0.1:8080/callback';\n\nconst verifier = randomBytes(32).toString('base64url'); // 43 caracteres; guarde até a volta\nconst state = randomBytes(16).toString('base64url');\nconst url = new URL('https://gamerfy.gg/autorizar');\nurl.search = new URLSearchParams({\n  response_type: 'code',\n  client_id: CLIENT_ID,\n  redirect_uri: REDIRECT_URI,\n  scope: 'chat:read gifts:read',\n  state,\n  code_challenge: createHash('sha256').update(verifier).digest('base64url'),\n  code_challenge_method: 'S256',\n}).toString();\n// Abra `url` no navegador. Na volta, confira o `state` e leia o `code`:\nasync function exchange(code) {\n  const res = await fetch('https://api.gamerfy.gg/oauth2/token', {\n    method: 'POST',\n    body: new URLSearchParams({ grant_type: 'authorization_code', client_id: CLIENT_ID, code, redirect_uri: REDIRECT_URI, code_verifier: verifier }),\n  });\n  if (!res.ok) throw new Error((await res.json()).error_description);\n  return res.json(); // { access_token, refresh_token, expires_in, scope }\n}\n```\n\n### 4. Renove, confira e devolva\n\n- A chave `gfp_…` vale **4 horas**. Antes de vencer, troque a `gfr_…` por um par\n  novo: `grant_type=refresh_token`, `refresh_token` e `client_id`. A `gfr_` vale\n  **60 dias**, renovados a cada troca, e **morre na hora em que é trocada**: usar\n  de novo uma `gfr_` já trocada tira a autorização inteira, porque é o sinal de\n  uma chave roubada. Guarde sempre o par mais novo.\n- `GET /oauth2/validate` com `Authorization: Bearer gfp_…` responde `client_id`,\n  `user_id`, `login`, `scopes` e `expires_in`.\n- `POST /oauth2/revoke` com `token` e `client_id` devolve aquela chave; com uma\n  `gfr_`, a autorização inteira.\n- Pedir a autorização de novo **soma** as permissões novas às que o streamer já\n  deu; as chaves antigas continuam valendo.\n- Um `client_secret` é aceito junto, mas não é preciso: um app de desktop não tem\n  onde guardá-lo, e quem protege a troca é o PKCE.\n\nOs erros de `/oauth2/token` e `/oauth2/revoke` seguem a RFC 6749\n(`invalid_request`, `invalid_client`, `invalid_grant`, `unsupported_grant_type`)\ne trazem `error_description`, que as bibliotecas de OAuth leem, ao lado do\n`message` de sempre. Um endereço de volta que não está cadastrado, ou uma\npermissão que não existe, nem abrem a tela: o streamer lê o motivo nela, e nada\nvolta para o seu app.\n\n### Permissões\n\n| Permissão | O que o streamer lê na tela | O que abre |\n|---|---|---|\n| `live:read` | Ver título, jogo e quantas pessoas assistem | `stream.*`, `channel.update`, `clip.ready` e o `snapshot.live` |\n| `chat:read` | Ler o chat da sua live | `channel.chat.*` e o `snapshot.chat` |\n| `chat:write` | Escrever no chat da sua live, com o nome do app | `POST /v1/chat/messages` |\n| `chat:moderate` | Apagar mensagens do chat da sua live | `DELETE /v1/chat/messages` |\n| `followers:read` | Saber quando alguém te segue | `channel.follow` |\n| `subscriptions:read` | Saber quando alguém assina | `channel.subscribe`, `channel.subscription.end` |\n| `gifts:read` | Saber quando alguém manda presente, com a mensagem | `channel.gift*`, `overlay.gifts_today` e o `snapshot.gifts_today` |\n| `goals:read` | Ver as suas metas | `channel.goal.*` |\n| `polls:read` | Ver as enquetes e os votos | `channel.poll.*` |\n| `polls:manage` | Abrir e encerrar enquetes | `POST` e `PATCH /v1/polls` |\n| `alerts:read` | Ver a fila dos seus alertas | `overlay.alert*` |\n| `raids:read` | Saber quando você manda ou recebe um raid | `channel.raid` |\n| `raids:manage` | Mandar e cancelar raids por você | `POST` e `DELETE /v1/raids` |\n\nA tela diz também, sempre, o que **nenhum** app pode: ver a senha ou os dados de\npagamento, usar a chave de transmissão, mexer no saldo ou pedir saque. Não existe\npermissão para isso.\n\n**App novo:** enquanto o Gamerfy não revisa o seu app, até **10 streamers** além\nde você podem autorizá-lo; o 11.º vê \"Este app ainda não foi revisado pelo\nGamerfy e já chegou ao limite de 10 streamers. Fale com quem o fez.\". Para pedir\na revisão, fale com o Gamerfy.\n\n## Canal de eventos\n\n```\nwss://api.gamerfy.gg/v1/events\n```\n\nO que acontece na live de um streamer, na hora, por WebSocket: chat, quem seguiu,\nquem assinou, presentes, título e jogo, audiência. O protocolo é o mesmo do\n**Gateway em tempo real** dos bots — quem já fez um bot já sabe usar:\n\n```\n← {\"op\":\"hello\",\"d\":{\"heartbeat_interval_ms\":30000,\"version\":1}}\n→ {\"op\":\"identify\",\"d\":{\"token\":\"gfp_…\"}}\n← {\"op\":\"dispatch\",\"t\":\"ready\",\"id\":\"…\",\"seq\":\"1727640423120-0\",\"d\":{\"session_id\":\"…\",\"broadcaster\":{…},\"scopes\":[…],\"overlay\":null,\"snapshot\":{…}}}\n→ {\"op\":\"heartbeat\"}\n← {\"op\":\"heartbeat_ack\"}\n← {\"op\":\"dispatch\",\"t\":\"channel.gift\",\"id\":\"1834021894356992001\",\"seq\":\"1727640423458-0\",\"d\":{…,\"created_at\":\"2026-09-29T21:47:03.120Z\"}}\n```\n\n- **A chave:** uma `gfp_…` da **Autorização de apps**. O canal entrega\n  só o que as permissões dela abrem. (As páginas de overlay do próprio Gamerfy\n  entram com a chave de overlay do streamer, `gfo_…`, que lê tudo e não escreve\n  nada.) Qualquer outra chave, `gfa_` e `gfb_` inclusive, fecha com 4002.\n- Todo evento vem em `dispatch`: `t` é o tipo, `id` o do evento — **o mesmo do\n  webhook**, para descartar repetições —, `seq` uma marca opaca para a retomada, e\n  `d` o `data` do evento com o `created_at` dentro.\n- `identify` em até 10 s depois do `hello`; `heartbeat` a cada\n  `heartbeat_interval_ms` (leia do `hello`); 75 s de silêncio fecham. Mais de 60\n  quadros por minuto fecham. Quadro acima de 4 KB é recusado pelo WebSocket (1009).\n\n### O `ready`\n\n```json\n{\n  \"session_id\": \"k3Jm9qLx0Zt2\",\n  \"broadcaster\": { \"id\": \"1834021894356992001\", \"login\": \"henrique\", \"name\": \"Henrique\" },\n  \"scopes\": [\"live:read\", \"chat:read\", \"gifts:read\"],\n  \"overlay\": null,\n  \"snapshot\": {\n    \"live\": { \"online\": true, \"title\": \"Ranked de terça\", \"game_id\": \"pubg\", \"game_name\": \"PUBG: Battlegrounds\", \"viewer_count\": 37, \"started_at\": \"2026-09-29T21:30:00.123Z\" },\n    \"gifts_today\": { \"total_cents\": 18600, \"top\": [{ \"user_login\": \"fulano\", \"user_name\": \"Fulano\", \"total_cents\": 7000 }] },\n    \"settings\": {},\n    \"goal\": null,\n    \"poll\": null,\n    \"alert\": null\n  }\n}\n```\n\nO `snapshot` é o estado da live no instante do `ready`, para você desenhar sem uma\nsegunda leitura. Cada campo só vem com a permissão que o abre: `live` com\n`live:read`, `gifts_today` com `gifts:read`.\n\nCom a chave de overlay (`gfo_…`), o `identify` pode dizer qual overlay a página é —\n`{\"op\":\"identify\",\"d\":{\"token\":\"gfo_…\",\"overlay\":\"alerts\"}}`, com `chat`, `alerts`,\n`gift_qr`, `goal`, `poll` ou `gift_rank` — e então o `ready` traz `overlay` com esse\nnome e `snapshot.settings` com o visual que o streamer salvou para ele (as cores, a\nposição, o tamanho). Sem `overlay`, ou com uma chave `gfp_…`, `settings` vem `{}`.\n\n### Retomada\n\nCaiu? Reconecte e, depois do `hello`, mande\n`{\"op\":\"resume\",\"d\":{\"token\":\"gfp_…\",\"session_id\":\"…\",\"seq\":\"<o último recebido>\"}}`.\nO canal repõe o que veio depois daquele `seq` e fecha com\n`{\"op\":\"dispatch\",\"t\":\"resumed\",\"d\":{\"replayed\":N}}`. A retomada funciona em\nqualquer servidor do Gamerfy. A sessão espera **2 minutos**, e o canal guarda os\n**200** eventos mais recentes da live por até **5 minutos**; fora disso, o\nfechamento é 4006 — mande `identify` e trate o `ready` novo como sempre.\n\n### Fechamentos\n\n| Código | Motivo | Reconectar? |\n|---|---|---|\n| 4000 | quadro que esta versão não aceita | corrija o cliente |\n| 4001 | 10 s sem `identify`, ou `heartbeat` antes dele | sim |\n| 4002 | chave inválida, vencida ou de outro tipo | **não** |\n| 4003 | o streamer tirou a autorização (ou trocou a chave de overlay) | **não**: peça a autorização de novo |\n| 4006 | a retomada não cobre mais esse trecho | sim, com `identify` |\n| 4008 | quadros demais | sim, com calma |\n| 4009 | 75 s sem `heartbeat` | sim |\n| 4010 | mais de **20** conexões com a mesma chave | **não**: feche as sobras |\n| 1001 | estamos saindo do ar (um deploy) | sim |\n| 1011 | falha do nosso lado | sim |\n\nAo reconectar, espere 1, 2, 4, 8, 16 e até 30 segundos, com uma variação\naleatória. Código desconhecido: tente de novo. Até **30 conexões novas por\nminuto** por endereço IP.\n\n### Os eventos\n\n| Tipo | Quando | `data`, além de `broadcaster_user_id`, `broadcaster_user_login` e `broadcaster_user_name` | Permissão |\n|---|---|---|---|\n| `stream.online` | a live entrou no ar | o item de `/v1/streams` (sem os três campos do broadcaster: ele já traz `user_id`) | `live:read` |\n| `stream.offline` | a live acabou | o do webhook `stream.offline` | `live:read` |\n| `channel.update` | o streamer mudou título ou jogo | `title`, `game_id`, `game_name` | `live:read` |\n| `stream.viewers` | a audiência mudou (no máximo a cada 5 s) | `viewer_count` | `live:read` |\n| `stream.peak` | a audiência passou o pico da live | `viewer_count`, `previous_peak` | `live:read` |\n| `clip.ready` | um clipe público da live ficou pronto | o item de `/v1/clips` | `live:read` |\n| `channel.chat.message` | mensagem nova no chat, inclusive as do sistema | `message_id`, `user_id`, `user_login`, `user_name`, `text`, `kind` (`default` ou `system`), `gift_amount_cents` (só na linha de um presente), `badges` (`[\"subscriber\"]`, ou `[\"bot\"]` na linha escrita por um app), `is_command`. Dois campos **podem não vir** — trate a falta como \"nada a mostrar\": `user_rank`, a patente de quem escreveu na carreira (`iron`, `bronze`, `silver`, `gold`, `platinum`, `diamond`, `master` ou `legend`; nunca na linha de um bot), e `highlight_rank`, só numa **mensagem destacada** — a patente com que ela foi mandada, em cujo metal o chat a desenha. Desde 12/10/2026 vêm mais dois, também opcionais: `user_seal`, o selo que a pessoa escolheu para aparecer antes do nome (`founder`, a etiqueta de Founder da Gamerfy; `partner`, a de parceiro oficial; `rank`, a patente; `plan`, o plano), e `user_plan`, só com `plan` — qual plano (`player`, `competitor` ou `creator`). Sem `user_seal`, desenhe a patente como antes. Desde 13/10/2026, junto de `user_rank` vem `user_rank_step`, o degrau dentro da patente (`0` é I, `1` II, `2` III; Lenda só tem `0`), para escrever o lugar inteiro (\"Platina II\"); sem ele, escreva só a patente | `chat:read` |\n| `channel.chat.message_update` | mensagem editada | `message_id`, `text` | `chat:read` |\n| `channel.chat.message_delete` | mensagem apagada | `message_id` | `chat:read` |\n| `channel.follow` | alguém seguiu | `user_id`, `user_login`, `user_name`, `followed_at` | `followers:read` |\n| `channel.subscribe` | o pagamento de uma assinatura foi confirmado | `user_id`, `user_login`, `user_name`, `ends_at`, `is_renewal` | `subscriptions:read` |\n| `channel.subscription.end` | uma assinatura acabou sem renovação | `user_id`, `user_login`, `user_name` | `subscriptions:read` |\n| `channel.gift` | o pagamento de um presente foi confirmado | `gift_id`, `user_id`, `user_login`, `user_name`, `tier` (`confetti`, `crown`, `rocket`), `amount_cents`, `message`, `message_state` | `gifts:read` |\n| `channel.gift.message_update` | o streamer liberou ou recusou a mensagem | `gift_id`, `message`, `message_state` (`shown` ou `rejected`) | `gifts:read` |\n| `channel.raid` | um raid saiu (chega a quem mandou e a quem recebeu) | `from_broadcaster_user_*`, `to_broadcaster_user_*`, `viewers` (as contas levadas), `message` | `raids:read` |\n| `overlay.gifts_today` | o ranking \"Presentes de hoje\" mudou | `total_cents`, `top` (até 3: `user_login`, `user_name`, `total_cents`) | `gifts:read` |\n| `channel.goal.begin` · `.progress` · `.achieved` · `.end` | uma meta nasceu, andou, chegou ao alvo, acabou | a meta (o corpo do webhook) | `goals:read` |\n| `channel.poll.begin` · `.progress` · `.end` | uma enquete abriu, recebeu votos, fechou | a enquete (o corpo do webhook) | `polls:read` |\n| `overlay.alert` · `.skip` · `.queue` | a fila de alertas tocou, pulou ou mudou | o alerta decidido pelo servidor | `alerts:read` |\n| `overlay.settings` | o streamer salvou o visual de um overlay | `kind` (o overlay) e `settings` (o visual, o mesmo do `snapshot.settings`) | a chave de overlay; `alerts:read` para `kind: alerts` |\n| `overlay.command` | o streamer pediu algo a um overlay agora | `kind` e `action`: `show` (o QR de presente, \"Mostrar agora\") ou `test_line` (o chat, \"Mandar linha de teste\") | a chave de overlay |\n\n- A mensagem de um presente só vem quando passou no filtro (`message_state:\n  shown`). Nos outros estados — `none` (sem mensagem), `filtered` (o filtro\n  barrou), `pending` (espera o streamer) — `message` é `null`.\n- Dinheiro é sempre inteiro em **centavos**; ids são strings.\n- O visual de um overlay traz `design`, de `\"01\"` a `\"30\"`: o estilo que o\n  streamer escolheu na galeria (nos alertas, um por tipo, em\n  `styles.<tipo>.design`, e ele vem dentro de cada `overlay.alert`). É o nome\n  de um desenho das páginas do Gamerfy; a sua página pode ignorar.\n- **Ainda chegando ao Gamerfy:** metas, enquetes, a fila de alertas, o ranking\n  `overlay.gifts_today` e a mensagem de presente que espera o streamer\n  (`pending` e `channel.gift.message_update`). Os tipos estão aqui para você já\n  preparar o código, e passam a vir quando cada um for lançado.\n- Numa mesma live o canal entrega na ordem em que os eventos aconteceram; entre\n  lives, a ordem real é o `created_at`. Um evento pode chegar duas vezes (numa\n  retomada): descarte pelo `id`.\n\n### Montar um overlay\n\nUm overlay é uma página que o OBS abre como fonte de navegador, transparente, por\ncima do jogo. Os do Gamerfy (`gamerfy.gg/overlay/alertas`, `…/chat`, `…/qr`…) são\nfeitos exatamente assim, e você pode fazer o seu do mesmo jeito. A página do site\n**Overlays para o OBS** (`gamerfy.gg/overlays`) mostra o caminho para quem não programa.\n\n1. **A chave.** Para a página de um streamer só, use a chave de overlay dele (`gfo_…`):\n   ele a copia na área Live, em Overlays, e ela só lê a live — nunca transmite, nunca\n   mexe em dinheiro. Para um app que atende muitos streamers, peça a cada um que\n   autorize o seu app e use a `gfp_…` dele. Guarde a chave **depois do `#`** do\n   endereço da página (`https://seu.site/alertas.html#gfo_…`): o que vem depois do\n   `#` nunca vai a servidor nenhum.\n2. **Conectar.** Abra `wss://api.gamerfy.gg/v1/events`. No `hello`, mande o\n   `identify` com a chave (e o `overlay`, se a página for um dos seis), e o `heartbeat`\n   a cada `heartbeat_interval_ms`.\n3. **Desenhar.** No `ready`, desenhe o estado do `snapshot` (o ranking, o visual). Cada\n   `dispatch` depois é um fato: `channel.gift` toca um presente, `overlay.alert` toca o\n   alerta que a fila do streamer decidiu — com a duração e o visual já dentro —,\n   `overlay.settings` troca o visual na hora.\n4. **Voltar.** Caiu? Espere 1, 2, 4… até 30 segundos e mande o `resume` com o último\n   `seq`. Fechou com 4002 ou 4003? A chave não vale mais (o streamer trocou ou tirou\n   a autorização): mostre isso na página e não reconecte.\n\n```js\nconst key = location.hash.slice(1); // gfo_…, depois do # do endereço\nconst ws = new WebSocket('wss://api.gamerfy.gg/v1/events');\nws.onmessage = ({ data }) => {\n  const frame = JSON.parse(data);\n  if (frame.op === 'hello') {\n    ws.send(JSON.stringify({ op: 'identify', d: { token: key, overlay: 'alerts' } }));\n    setInterval(() => ws.send(JSON.stringify({ op: 'heartbeat' })), frame.d.heartbeat_interval_ms);\n  }\n  if (frame.t === 'overlay.alert') showAlert(frame.d);\n};\n```\n\n> **Nunca mostre o endereço com a chave na transmissão.** Quem tiver o endereço vê\n> tudo o que ele mostra. Se vazar, o streamer troca a chave em Overlays, e os\n> endereços antigos param na hora (o canal fecha com 4003).\n\n## Raids\n\nUm app que o streamer autorizou com `raids:manage` manda um raid da live dele para outra\n(`POST /v1/raids` com `from_broadcaster_id` e `to_broadcaster_id`) e cancela o raid em contagem\n(`DELETE /v1/raids?broadcaster_id=`). São as regras do painel do Gamerfy: a contagem de 60 segundos\naparece para todo mundo e o raid sai sozinho no fim dela (ou depois do sim de quem pergunta a cada\nraid); a live precisa de pelo menos 5 contas assistindo, a conta de 7 dias ou mais, 1 hora entre dois\nraids para o mesmo canal e no máximo 10 raids em 24 horas; e quem recebe precisa aceitar raids de quem\nmanda. Cada recusa vem com o seu código (`raid_too_few`, `raid_not_accepted`, `raid_daily_limit`…) e a\nfrase que o painel mostra. Com `raids:read`, o canal de eventos e os webhooks dizem `channel.raid`\nquando um raid sai, para os dois lados.\n\n## Enquetes\n\nUm app que o streamer autorizou com `polls:manage` abre e encerra enquetes na live dele, como o painel\ndo Gamerfy faria; com `polls:read`, lê a aberta e as anteriores. Sempre com a chave do streamer\n(`Authorization: Bearer gfp_…`), e `broadcaster_id` tem de ser o dono da chave (senão,\n`403 broadcaster_mismatch`). As três respostas vêm no molde da Twitch, `{ \"data\": [ … ] }`, e cada\nitem é a mesma enquete que os eventos `channel.poll.*` levam.\n\n```bash\ncurl -X POST https://api.gamerfy.gg/v1/polls \\\n  -H \"Authorization: Bearer gfp_…\" -H \"Content-Type: application/json\" \\\n  -d '{\"broadcaster_id\":\"1834021894356992012\",\"title\":\"Qual mapa agora?\",\"choices\":[{\"title\":\"Mirage\"},{\"title\":\"Inferno\"}],\"duration\":180}'\n```\n\n- **Abrir** (`POST /v1/polls`): `title` de 1 a 60 caracteres, `choices` de 2 a 5 (cada `title` de 1 a 25,\n  sem repetir), `duration` de 15 a 1800 segundos (sem o campo, a enquete não tem prazo: fica aberta até o `PATCH` ou o fim da live, e `ends_at` vem `null`) e, se quiser, `eligibility`: `everyone` (o padrão),\n  `followers` ou `subscribers`. Só com a live no ar (`409 creator_not_live`) e uma por vez\n  (`409 poll_open`). Cada campo fora das regras tem o seu código: `poll_question_invalid`,\n  `poll_options_invalid`, `poll_option_invalid`, `poll_option_repeated`, `poll_duration_invalid`.\n- **Encerrar** (`PATCH /v1/polls`, `{ \"broadcaster_id\", \"id\", \"status\": \"TERMINATED\" }`): fecha agora,\n  com o resultado contado no servidor; uma que já fechou é `409 poll_closed`.\n- **Ler** (`GET /v1/polls?broadcaster_id=`): a aberta primeiro e as dez anteriores; `&id=` traz uma só.\n\nQuem vota é quem assiste, no topo do chat da live: uma conta, um voto, sem trocar. Para acompanhar a\ncontagem, ouça `channel.poll.progress` no canal de eventos (uma vez por segundo, no máximo) — não\npergunte de tempos em tempos.\n\n## Chat: escrever e moderar\n\nLer o chat é pelo canal de eventos (`chat:read`). Com mais duas permissões o seu app também **fala** e\n**modera** — o que um bot de moderação, um sorteio ou um comando `!regras` precisam. As duas rotas usam a\nchave do streamer (`Authorization: Bearer gfp_…`), e `broadcaster_id` tem de ser o dono da chave.\n\n```bash\n# escrever (chat:write)\ncurl -X POST https://api.gamerfy.gg/v1/chat/messages \\\n  -H \"Authorization: Bearer gfp_…\" -H \"Content-Type: application/json\" \\\n  -d '{\"broadcaster_id\":\"1834021894356992012\",\"message\":\"Lembrete: sem spoiler no chat!\"}'\n# → { \"data\": [ { \"message_id\": \"98765432109876543\", \"is_sent\": true } ] }\n\n# apagar uma linha (chat:moderate)\ncurl -X DELETE \"https://api.gamerfy.gg/v1/chat/messages?broadcaster_id=1834021894356992012&message_id=98765432109876543\" \\\n  -H \"Authorization: Bearer gfp_…\"\n# → 204\n```\n\n- **Quem fala é o bot do app**, nunca o streamer. Em **Configurações → Desenvolvedor**, no seu app, aperte\n  **Ligar bot**: é essa conta que escreve, com o nome e o ícone do app e o selo **BOT** ao lado. Um app sem\n  bot não tem como falar: `409 app_has_no_bot`. (O token de bot, `gfb_…`, não entra aqui: ele é a porta dos\n  servidores. No chat da live quem autoriza é o streamer, com a chave `gfp_…`.)\n- **A linha**: texto simples, de 1 a 500 caracteres, sem começar com `/`. Ela chega a todo mundo como qualquer\n  outra — no app, no site, no overlay de chat do OBS — e ao canal de eventos como `channel.chat.message` com\n  `badges: [\"bot\"]`. É por aí (ou pelo `user_id`) que o seu app reconhece as próprias linhas e não responde a si\n  mesmo.\n- **O limite**: 20 linhas a cada 30 segundos por app **e chat** (`429 rate_limited`, com a espera em\n  `details.retry_after_ms` e em `Retry-After`). Uma linha recusada não gasta vaga.\n- **Apagar** tira a linha para todo mundo na hora, como o streamer faria, e o canal diz\n  `channel.chat.message_delete`. Só linhas **daquele chat**: o id de uma mensagem de outro lugar — um servidor,\n  uma conversa, outra live — é o mesmo `404 message_not_found` de um id que não existe. O `message_id` é\n  obrigatório: não há como limpar o chat inteiro.\n- Um streamer que nunca abriu a área Live ainda não tem chat: `404 chat_not_found`. Se ele bloqueou o bot,\n  escrever responde `403 blocked`.\n\nUm exemplo completo, que você roda na sua máquina ou hospeda num container:\n[GAMERFY-MODERATOR-BOT](https://github.com/trojahnn/GAMERFY-MODERATOR-BOT) — um moderador com IA que lê o chat,\napaga o que foge das regras e avisa quem escreveu. Um processo só atende todos os streamers que o instalaram.\n\n## Webhooks\n\nEm vez de consultar `/v1/streams` e `/v1/videos` de minuto em minuto, registre\numa URL com `POST /v1/webhooks` e o Gamerfy faz um `POST` nela a cada evento. É\no espírito do EventSub da Twitch sem a cerimônia: sem *challenge* na inscrição\ne sem tipos de transporte. É um `POST` assinado, e pronto.\n\nOs eventos e o corpo de cada um estão na seção **Webhooks** da referência. Todo\ncorpo tem o formato `{ id, type, created_at, data }`. Nos quatro de sempre\n(`stream.online`, `stream.offline`, `video.ready`, `clip.ready`), `data` é\nexatamente o item que a rota correspondente devolveria naquele instante; nos da\nlive, é o mesmo `data` do **Canal de eventos**, com o mesmo `id`.\n\n### Os eventos da live, só com autorização\n\n| Chega de toda live pública | Só das lives de quem autorizou o seu app |\n|---|---|\n| `stream.online`, `stream.offline`, `video.ready`, `clip.ready`, `channel.update` | `channel.follow` (`followers:read`), `channel.subscribe` e `channel.subscription.end` (`subscriptions:read`), `channel.gift` e `channel.gift.message_update` (`gifts:read`), `channel.goal.begin` · `.progress` · `.achieved` · `.end` (`goals:read`), `channel.poll.begin` · `.progress` · `.end` (`polls:read`) |\n\nUm aviso de presente, de seguidor ou de assinatura **só sai para o seu app se o\nstreamer autorizou o app com a permissão do tipo** (veja **Autorização de apps**).\nSem autorização, nada sai; e quando ele tira o acesso em Conexões, param os novos\ne também as tentativas de um aviso que já estava na fila. O mesmo webhook pode\nouvir as lives de todos os streamers que autorizaram o app. Um webhook aceita até\n**32** tipos na lista.\n\n### Verificar a assinatura\n\nCada entrega traz:\n\n| Cabeçalho | Valor |\n|---|---|\n| `Gamerfy-Webhook-Id` | o `id` do evento |\n| `Gamerfy-Webhook-Timestamp` | segundos Unix do envio desta tentativa |\n| `Gamerfy-Signature` | `sha256=<hex>`: HMAC-SHA256 com o `secret`, sobre `\"{id}.{timestamp}.{corpo bruto}\"` |\n\nO corpo bruto é o JSON tal como chegou, byte a byte: **não** faça `JSON.parse`\ne serialize de novo antes de verificar. Recuse um `timestamp` com mais de\nalguns minutos de diferença, para que um envio capturado não possa ser\nreaproveitado.\n\n```js\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport express from 'express';\n\nconst SECRET = process.env.GAMERFY_WEBHOOK_SECRET; // o `secret` do registro\nconst MAX_SKEW_S = 300;\n\nconst app = express();\n\n// `express.raw`: o corpo BRUTO é o que foi assinado.\napp.post('/gamerfy', express.raw({ type: 'application/json' }), (req, res) => {\n  const id = req.get('Gamerfy-Webhook-Id') ?? '';\n  const timestamp = req.get('Gamerfy-Webhook-Timestamp') ?? '';\n  const signature = req.get('Gamerfy-Signature') ?? '';\n  const rawBody = req.body.toString('utf8');\n\n  const expected =\n    'sha256=' + createHmac('sha256', SECRET).update(`${id}.${timestamp}.${rawBody}`).digest('hex');\n  const given = Buffer.from(signature);\n  const wanted = Buffer.from(expected);\n  if (given.length !== wanted.length || !timingSafeEqual(given, wanted)) {\n    return res.status(401).end();\n  }\n  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > MAX_SKEW_S) {\n    return res.status(401).end();\n  }\n\n  const event = JSON.parse(rawBody); // { id, type, created_at, data }\n  // …guarde event.id para descartar repetições e trate event.type/event.data\n  res.status(204).end();\n});\n\napp.listen(8080);\n```\n\n### Entrega, tentativas e desligamento\n\n- Responda **2xx em até 10 segundos**. Qualquer outra coisa (outro status, um\n  redirecionamento, timeout, recusa de conexão) conta como falha. Processe fora\n  do ciclo do pedido se o trabalho for longo.\n- Uma entrega que falha é repetida **5 vezes**, com espera exponencial a partir\n  de 60 s (1, 2, 4, 8 e 16 minutos).\n- O `id` do evento é o mesmo em todas as tentativas e em todos os webhooks que\n  o recebem: use-o para descartar duplicatas.\n- Falhas **seguidas** somam em `failures` (a primeira resposta 2xx zera). Na\n  **20.ª** o webhook é desligado: `disabled_at` deixa de ser `null`, nada mais é\n  enviado e o que estava na fila é descartado. Para voltar, apague e registre\n  de novo.\n- A ordem de chegada não é garantida entre eventos diferentes; o `created_at`\n  do evento diz a ordem real.\n- Um evento cujas 5 tentativas falharam é perdido: se a sua URL ficou fora do\n  ar, confira `/v1/streams` e `/v1/videos` ao voltar.\n\nRegras da URL: **`https://`** obrigatório, sem usuário e senha, sem fragmento\n(`#`), host público (nada de `localhost`, `10.x`, `192.168.x`, `127.x`…) e até\n2.048 caracteres.\n\n## Erros e limites\n\nTodo erro tem o mesmo formato:\n\n```json\n{ \"error\": \"invalid_token\", \"message\": \"token de app ausente, inválido ou expirado\" }\n```\n\n`error` é o código estável: compare com ele, nunca com a mensagem. `message` é\npara humanos, em pt-BR, e pode mudar. `details` aparece só em erros de\nvalidação.\n\n| Status | `error` | Quando |\n|---|---|---|\n| 400 | `invalid_body` | corpo malformado, campo faltando, URL de webhook fora das regras ou evento desconhecido |\n| 400 | `invalid_query` | parâmetro recusado: nenhum filtro, mais de 100 valores, `first` fora de 1..100, id não numérico, data fora do ISO 8601 |\n| 400 | `unsupported_grant_type` | `grant_type` diferente de `client_credentials`, `authorization_code` e `refresh_token` |\n| 400 | `invalid_request` · `invalid_grant` | *(autorização)* pedido incompleto; código vencido, já usado, de outro app, com outro `redirect_uri` ou com `code_verifier` que não confere; `refresh_token` já trocado ou vencido — com `error_description` |\n| 400 | `invalid_cursor` | `after` corrompido |\n| 400 | `bot_account` | *(bots)* mensagem privada para uma conta de bot — o próprio inclusive |\n| 401 | `invalid_client` | `client_id` ou `client_secret` errados — ou, na autorização, app com a autorização desligada pelo Gamerfy |\n| 401 | `invalid_token` | bearer ausente, com formato errado, desconhecido ou expirado — ou, nas rotas de bot, o mesmo com o token `Bot gfb_…` |\n| 401 | `bot_token_expired` | *(bots)* token de bot com 90 dias sem uso; gere outro na aba Desenvolvedor |\n| 403 | `missing_scope` | a autorização do streamer não inclui a permissão que a rota pede; `details.scope` diz qual |\n| 403 | `missing_permission` | *(bots)* falta uma permissão do bot naquele servidor ou sala; `details.permission` nomeia qual |\n| 403 | `no_shared_guild` · `bot_dms_disabled` · `blocked` | *(bots)* mensagem privada para quem não está em nenhum servidor com o bot, desligou as mensagens privadas de bots, ou tem bloqueio com ele |\n| 404 | `not_found` | rota que não existe |\n| 404 | `webhook_not_found` | webhook que não é do seu app |\n| 404 | `channel_not_found` | *(bots)* id que não é sala, sala que não existe, conversa direta, sala de tipo errado para a rota, ou sala de servidor onde o bot não está — todas respondem igual, de propósito: um 404 não conta a um estranho o que existe |\n| 404 | `voice_session_not_found` | *(bots)* sessão de voz que já acabou, ou de outro bot |\n| 404 | `user_not_found` | *(bots)* mensagem privada para um id que não é de conta nenhuma |\n| 409 | `webhook_limit_reached` | sexto webhook do mesmo app |\n| 413 | `payload_too_large` | corpo acima de 64 KB (a oferta WHIP inclusive) |\n| 415 | `unsupported_media_type` | *(bots)* corpo da entrada de voz que não é `application/sdp`, ou está vazio |\n| 429 | `rate_limited` | mais de 800 pedidos no minuto — ou, nos bots, mais de 5 mensagens em 5 s no mesmo canal, mais de 20 mensagens privadas por minuto, ou mais de 6 entradas de voz por minuto na mesma sala; `details.retry_after_ms` diz quanto esperar |\n| 502 · 503 | `voice_unavailable` | *(bots)* o servidor de mídia recusou a entrada (502) ou não respondeu (503) |\n| 503 | `bots_unavailable` | *(bots)* este nó do backend subiu sem a API de bots; é do servidor e não do seu pedido — repita (em produção todo nó liga os bots) |\n| 503 | (página de manutenção) | durante uma atualização; `Retry-After: 600` |\n\n**Rate limit:** 800 pedidos por minuto por app, em janela fixa. Os tokens de um\nmesmo app dividem o limite. Toda resposta autenticada traz `Ratelimit-Limit`,\n`Ratelimit-Remaining` e `Ratelimit-Reset` (segundos até a janela virar). Ao\nreceber 429, espere `Ratelimit-Reset` segundos.\n\n**Retenção:** cada replay fica **14 dias** a partir do fim da live. Depois disso\nsome de `/v1/videos` e a página responde 404. O streamer pode apagar um replay\nantes do prazo.\n\n## Bots\n\nUm bot é uma conta que o seu app opera: entra em servidores, escreve, modera e\nfala numa sala de voz. É a outra porta desta API — token próprio\n(`Authorization: Bot gfb_…`), rotas próprias (na referência, sob a tag\n**Bots**) e um gateway WebSocket que entrega o que acontece na hora.\n\nA conta de bot não é uma pessoa: não aparece na busca, não tem página de perfil,\nnão segue ninguém, não recebe mensagem direta e não entra nas métricas de\npessoas. Ela existe dentro dos servidores onde foi adicionada — e pode **mandar**\numa mensagem privada a quem está num desses servidores (veja **Mensagem\nprivada** abaixo).\n\n### Criar um bot\n\nNo Gamerfy, em **Configurações → Desenvolvedor**, no seu app:\n\n1. **Ligar bot** cria a conta. O `@usuário` sai do nome do app (minúsculas,\n   espaços viram `_`, sem acento, com sufixo `_2`, `_3`… em conflito); o nome\n   visível e o avatar são o nome e o ícone do app. Renomear o app muda o nome\n   visível do bot, nunca o `@usuário`.\n2. O **token de bot** aparece **uma única vez** (o Gamerfy guarda só o\n   SHA-256). **Gerar novo token** invalida o anterior na hora e fecha a conexão\n   do gateway com o código 4003.\n3. O link de adicionar só liga com o **perfil completo**: bot ligado, nome,\n   ícone, descrição e política de privacidade. A partir daí o endereço a\n   divulgar é `https://gamerfy.gg/bot/<client_id>` — quem abrir escolhe o\n   servidor e marca as permissões.\n\nUm bot entra em até **100 servidores** por enquanto. O 101.º é recusado na tela\nde quem está adicionando (`409 bot_guild_limit`), e não numa rota de `/v1`: quem\nadiciona é uma pessoa, não o bot. Cada conta pode ter até 10 apps, e cada app um\nbot.\n\n| Credencial | Formato | Onde guardar |\n|---|---|---|\n| token de bot | `gfb_` + 40 caracteres `[A-Za-z0-9]` | segredo; vence com 90 dias sem uso |\n\n**O token vence por inatividade.** Cada conexão do gateway e cada chamada da\nAPI renova o prazo; 90 dias sem nenhuma das duas e ele deixa de valer: as rotas\nrespondem `401 bot_token_expired` e o gateway fecha com 4002. A aba\nDesenvolvedor mostra \"Token expirado por inatividade\" com o botão de gerar\noutro. Um bot que fica conectado nunca expira.\n\n**Apagar o app aposenta o bot:** ele sai de todos os servidores, o gateway\nfecha com 4005 e a conta vira \"Bot removido\". As mensagens que ele escreveu\nficam nos canais com esse autor, e quem tem Gerenciar mensagens ainda pode\napagá-las.\n\n### Autenticação\n\n```bash\ncurl https://api.gamerfy.gg/v1/bot/@me \\\n  -H 'Authorization: Bot gfb_…'\n```\n\nO esquema é **`Bot`**, não `Bearer`: o token de app (`gfa_`) não abre as rotas\nde bot, e o token de bot não abre as de app. Ausente, malformado ou revogado é\n`401 invalid_token`; vencido por inatividade é `401 bot_token_expired`.\n\nQuatro limites, os quatro com o código `rate_limited` e a espera em\n`details.retry_after_ms` (também em `Retry-After`, em segundos):\n\n| Limite | Escopo | Onde ler |\n|---|---|---|\n| 800 pedidos por minuto | por app, em janela fixa; contagem própria, à parte da do token `gfa_` | `Ratelimit-Limit`, `Ratelimit-Remaining` e `Ratelimit-Reset` em toda resposta |\n| 5 mensagens a cada 5 segundos | por bot **e canal**: responder em dez canais não atrasa nenhum | a mensagem do 429 |\n| 20 mensagens privadas por minuto | por bot, **somando todas as pessoas** | a mensagem do 429 |\n| 6 entradas de voz por minuto | por bot **e sala**: tocar em dez salas não atrasa nenhuma | a mensagem do 429 |\n\nUma mensagem **recusada não gasta vaga**: um corpo inválido é barrado antes do\ncontador, e a recusa por permissão devolve a vaga que tomou. As cinco são cinco\nmensagens, não cinco tentativas. Só o erro 500 fica com a vaga, porque ninguém\npode prometer que a mensagem não entrou.\n\nNa voz, a recusa **na porta** também não gasta vaga: corpo que não é oferta SDP\n(415), sala inexistente ou de servidor sem o bot (404) e falta de Ver canais,\nConectar ou Falar (403). Mas **toda entrada que passa da porta gasta vaga**,\naceita ou recusada pelo servidor de mídia (502, 503), porque o que custa é a\nprópria entrada — a conexão com o servidor de mídia e o entra-e-sai na sala das\npessoas que estão lá.\n\n### Permissões\n\nO bot tem uma concessão **própria** em cada servidor, dada por quem o adicionou\ne mudável depois em \"Permissões do bot\", no cartão do membro. Ela não vem de\ncargos: `@everyone` e os cargos do servidor não somam nada para um bot, que\nrecebe só o cargo **Bot** — criado e mantido pelo sistema, com permissão zero.\n\nAs permissões numa sala, em ordem:\n\n1. base: a concessão do bot naquele servidor;\n2. **Administrador** na base: tudo, e as sobrescritas não são lidas;\n3. sobrescrita do `@everyone` na sala: só o que ela **nega** vale (uma sala\n   fechada fecha para o bot; um `allow` para todos não abre o que o admin não\n   deu);\n4. sobrescritas dos cargos **que o bot tem** (o cargo Bot, ou outro que o\n   admin lhe deu): valem nos dois sentidos, somadas como as de uma pessoa (um\n   `allow` de um cargo vence o `deny` de outro); as de cargos que o bot não tem\n   não são lidas;\n5. sobrescrita **do próprio bot**: vale nos dois sentidos e decide. Um bot entra\n   numa **sala privada** de dois jeitos — a lista de membros da sala escreve\n   essa sobrescrita, ou o admin libera a sala ao cargo Bot.\n\n`permissions` é sempre uma string em decimal, no servidor\n(`GET /v1/bot/guilds`) e em cada sala (`GET /v1/guilds/{guild_id}/channels`,\n`ready`, `guild.permissions_updated`). Teste bits, nunca compare o valor\ninteiro:\n\n| Permissão | Bit | Decimal | O que abre para o bot |\n|---|---|---|---|\n| Ver canais (`VIEW_CHANNEL`) | 0 | 1 | enxergar a sala; sem ela a sala não aparece em lugar nenhum |\n| Enviar mensagens (`SEND_MESSAGES`) | 1 | 2 | `POST /v1/channels/{channel_id}/messages` |\n| Gerenciar mensagens (`MANAGE_MESSAGES`) | 2 | 4 | apagar mensagem de outra pessoa (a própria dispensa) |\n| Expulsar membros (`KICK_MEMBERS`) | 6 | 64 | expulsar e banir |\n| Conectar (`CONNECT`) | 8 | 256 | entrar na sala de voz |\n| Falar (`SPEAK`) | 9 | 512 | publicar áudio (sem ela o bot não entra na sala; tirada no meio da chamada, corta o microfone na hora) |\n| Ler mensagens (`READ_MESSAGE_CONTENT`) | 13 | 8192 | receber o **texto** das mensagens dos canais que enxerga |\n| Administrador (`ADMINISTRATOR`) | 30 | 1073741824 | todas, em todas as salas |\n\nO nome entre parênteses é a constante que um `403 missing_permission` devolve em\n`details.permission` — é por ela que se volta desta tabela para a permissão que\nfaltou. O rótulo em português é o que a pessoa que adiciona o bot vê na tela.\n\nExemplo: `\"permissions\": \"8963\"` é Ver canais + Enviar mensagens + Conectar +\nFalar + Ler mensagens.\n\n**Ler mensagens** só existe para bot (uma pessoa lê com Ver canais) e nunca vai\nnum cargo nem numa sobrescrita. Sem ela, `message.created` e `message.updated`\nchegam com `content: null` e `attachments` vazio. O filtro roda no servidor — o\ntexto não sai daqui — e libera em quatro casos: o bot tem Ler mensagens na sala\n(ou Administrador), **a mensagem é um comando (o texto começa com `/`)**, o bot\nestá entre as menções, ou o bot é o autor.\n\nComando e menção sempre chegam inteiros: um bot que só toma `/comandos` ou\nresponde quando é chamado **não precisa** de Ler mensagens. `mentions` também\nvem sempre como a mensagem o escreveu, então um bot que recebe `content: null`\nainda sabe que não era com ele. A permissão total só faz falta a um bot que\nprecisa ler TUDO que passa no canal (um moderador, por exemplo).\n\nOs outros bits do servidor (Gerenciar canais, Gerenciar servidor, Gerenciar\ncargos, Criar convites, Gerenciar apelidos, Silenciar membros) podem ser\nconcedidos ao bot, mas nenhuma rota desta fase os exige. Transmitir tela e câmera\ndeixa um bot que ouve a sala compartilhar a tela (ver \"Ouvir a sala\").\n\n### Gateway em tempo real\n\n```\nwss://api.gamerfy.gg/v1/gateway\n```\n\nUm WebSocket comum, quadros JSON, sem compressão e sem intents: o bot recebe o\nque as permissões dele deixam ver. A conversa:\n\n```\nwss://api.gamerfy.gg/v1/gateway?resume=1\n← {\"op\":\"hello\",\"d\":{\"heartbeat_interval_ms\":30000,\"version\":1}}\n→ {\"op\":\"identify\",\"d\":{\"token\":\"gfb_…\"}}\n← {\"op\":\"dispatch\",\"t\":\"ready\",\"id\":\"1834021894356992001\",\"d\":{\"bot\":{…},\"guilds\":[…],\"session_id\":\"k3Jm9qLx0Zt2.1834021894356992001\"},\"seq\":0}\n→ {\"op\":\"heartbeat\"}\n← {\"op\":\"heartbeat_ack\"}\n← {\"op\":\"dispatch\",\"t\":\"message.created\",\"id\":\"1834021894356992002\",\"d\":{…},\"seq\":1}\n```\n\n- O `identify` tem **10 segundos** a partir do `hello`; depois dele, um\n  `heartbeat` a cada `heartbeat_interval_ms` (30 s hoje — leia do `hello`, não\n  fixe no código).\n- Todo evento vem em `dispatch`, com `t` (o nome), `id` (um snowflake deste\n  quadro) e `d` (o corpo).\n- **Uma conexão por bot.** Conectar de novo derruba a anterior com 4004; não\n  abra duas.\n- Quadro binário ou fora do protocolo: 4000. Mais de 60 quadros por minuto:\n  4008. Silêncio de 75 s: 4009. Quadro acima de **4 KB** é recusado pelo próprio\n  WebSocket, com o 1009 do padrão (nenhum quadro deste protocolo chega perto\n  disso).\n- Conectar marca o bot **online** (a bolinha na lista de membros do servidor).\n- **Retomada.** Com `?resume=1` no endereço, o `ready` traz `session_id` e todo\n  `dispatch` traz `seq` (o `ready` é 0; depois 1, 2, 3…). Caiu? Reconecte e, depois\n  do `hello`, mande `{\"op\":\"resume\",\"d\":{\"token\":\"gfb_…\",\"session_id\":\"…\",\"seq\":<último\n  recebido>}}`: o gateway guarda a sessão por **2 minutos** e até **1.000 quadros ou 1 MB**, repõe\n  os `dispatch` com `seq` maior que o seu e fecha a reposição com\n  `{\"op\":\"dispatch\",\"t\":\"resumed\",\"d\":{\"replayed\":N}}`. Presença: offline enquanto\n  a sessão está parada, online de novo no `resumed`. Se a sessão não existe mais\n  (venceu, estourou, o servidor reiniciou, outro processo seu fez `identify`, o bot\n  pediu um `seq` que a sessão não cobre — uma retomada que ficou para trás —, ou o nó\n  atingiu o teto de 500 sessões estacionadas e a mais antiga caiu), o\n  fechamento é **4006** — mande `identify` e trate o `ready` como sempre. Sem\n  `?resume=1` nada disso aparece e o fio é o de sempre.\n\n| Evento | `d` |\n|---|---|\n| `ready` | `bot` (a conta, com `app_id`) e `guilds`: cada servidor com as salas visíveis, as permissões do bot em cada uma e os estados de voz dessas salas |\n| `guild.joined` | o servidor inteiro, no formato do `ready` |\n| `guild.left` | `guild_id` — expulso, banido, ou o servidor apagado; os eventos daquele servidor param |\n| `guild.permissions_updated` | `guild_id`, `permissions` no servidor e `channels` (`id` e `permissions` de cada sala visível) |\n| `channel.created` / `channel.updated` | `guild_id` e `channel` (`id`, `guild_id`, `type`, `name`, `position`, `permissions`) — só salas visíveis |\n| `channel.deleted` | `guild_id` e `channel_id` |\n| `member.joined` | `guild_id` e `member` (o usuário com `bot`, `nickname`, `joined_at`) |\n| `member.left` | `guild_id` e `user_id` |\n| `message.created` / `message.updated` | a mensagem: `id`, `guild_id`, `channel_id`, `author`, `content`, `mentions`, `attachments`, `created_at`, `edited_at` |\n| `message.deleted` | `id`, `guild_id` e `channel_id` |\n| `voice_state.updated` | `guild_id`, `user_id` e `channel_id` (ou `null` ao sair) — só salas visíveis |\n\nUma sala que passa a ser **visível** para o bot (porque a permissão dele mudou)\nchega como `guild.permissions_updated`, e não como `channel.created` — esse\ncorpo traz id e permissões, não o nome. Para o nome, releia\n`GET /v1/guilds/{guild_id}/channels`.\n\nOs `voice_states` do `ready` e os `voice_state.updated` vêm do espelho de voz do\nbackend, alimentado pelos webhooks do LiveKit — que são melhor-esforço. Para agir\nsobre a sala de alguém (um `/play` que entra na sala de quem pediu), leia o\nservidor de novo com **`GET /v1/guilds/{guild_id}`**: a resposta tem a mesma forma\ndo `ready` por servidor (concessão, salas visíveis e `voice_states`), e o backend\nconfere quem está nas salas contra o próprio LiveKit antes de responder (no máximo\numa vez a cada 5 s por servidor). No SDK é `bot.fetchGuild(guildId)`, que refaz o\ncache com a resposta.\n\n| Código | Motivo | Reconectar? |\n|---|---|---|\n| 4000 | quadro que esta versão não aceita (op desconhecido, forma errada, binário, segundo `identify`) | corrija o cliente |\n| 4001 | passou de 10 s sem `identify`, ou mandou `heartbeat` antes dele | sim |\n| 4002 | token inválido ou vencido por inatividade | **não**: gere outro na aba Desenvolvedor |\n| 4003 | o token foi trocado | **não**: use o novo |\n| 4004 | outra conexão do mesmo bot assumiu | **não**: é o seu outro processo |\n| 4005 | app apagado ou bot desligado | **não** |\n| 4006 | a sessão do `resume` não existe mais | sim, com `identify` |\n| 4008 | quadros demais | sim, com calma |\n| 4009 | 75 s sem `heartbeat` | sim |\n| 1001 | estamos saindo do ar (um deploy) | sim |\n| 1011 | falha do nosso lado no meio de um quadro bom | sim |\n\nAo reconectar, espere 1, 2, 4, 8, 16 e até 30 segundos, com uma variação\naleatória — é o que o SDK faz. Códigos novos podem aparecer: trate qualquer\ndesconhecido como \"tentar de novo\", nunca como fatal.\n\n### Voz\n\nO bot fala pelo **WHIP** (RFC 9725), o mesmo padrão que o app usa para mandar\nlive. O áudio vai do bot direto ao servidor de mídia; o Gamerfy é o portão, não\no cano.\n\n```bash\ncurl -X POST 'https://api.gamerfy.gg/v1/channels/1834021894356992010/voice' \\\n  -H 'Authorization: Bot gfb_…' \\\n  -H 'Content-Type: application/sdp' \\\n  --data-binary @offer.sdp\n```\n\n- A oferta é de um WebRTC **só de envio**, com os candidatos ICE **já juntos**:\n  não existe `PATCH` (ICE trickle) nesta fase. Até 64 kB.\n- Exige Ver canais + **Conectar** + **Falar** na sala (`403 missing_permission`,\n  com o bit em `details.permission`). Sala inexistente, que não é de voz, ou de\n  servidor onde o bot não está: `404 channel_not_found`.\n- **Falar é seguida durante a chamada.** Perder *Falar* com o bot numa sala corta\n  o microfone **na hora** — o servidor de mídia derruba a faixa; o bot fica na\n  sala, mudo (o SDK interrompe a faixa e `play()` rejeita `missing_permission`).\n  Recuperar *Falar* devolve o microfone na hora: o SDK refaz a sessão WHIP\n  sozinho; quem fala WHIP direto refaz a oferta (um `POST` novo — a sessão\n  antiga pode ser encerrada pelo `DELETE` dela).\n- O 201 traz a resposta SDP em `application/sdp` e três cabeçalhos:\n  `Location` com a sessão (`/v1/voice/sessions/<uuid>`), `Link` com os\n  servidores ICE e `ETag` da sessão ICE.\n- **`Location` é relativo à base da API**, não à origem do pedido: resolva\n  contra a sua `apiUrl` (com prefixo de caminho, se houver) e faça `DELETE`\n  nesse endereço para sair — 204, e 404 se a sessão já acabou ou é de outro bot.\n- **`Link` traz os servidores ICE do servidor de mídia** (`stun:`, `turn:` e `turns:`,\n  com `username`/`credential` nos TURN; o TURN/TLS vem na porta que o Gamerfy publica,\n  5349). Pode trazer vários valores numa linha, e um endereço TURN tem vírgula dentro\n  das aspas: use um parser de `Link`, nunca `split(',')`. Atrás de NAT restritivo ou\n  numa rede sem UDP até o servidor de mídia, a primeira oferta não conecta — os\n  servidores só chegam na resposta: refaça a entrada (`DELETE` na sessão e um `POST`\n  novo com uma oferta gerada só com esses servidores, em modo relay). O SDK faz isso\n  sozinho (`voice.transport`); cada `POST` conta no limite de 6 entradas por minuto.\n- Por esta entrada o bot **só fala**: ele não recebe o áudio da sala. Para ouvir,\n  veja **Ouvir a sala** abaixo.\n- **Uma sala por servidor, uma em cada servidor.** Entrar em outra sala do mesmo\n  servidor tira o bot da primeira; entrar numa sala de outro servidor não mexe em\n  nada, e cada sessão se desliga pelo seu próprio `DELETE`.\n- Servidor de mídia fora do ar: `503 voice_unavailable`; entrada recusada por\n  ele: `502 voice_unavailable`. O resto do bot continua funcionando.\n- Na sala, a conta do bot passa pelos mesmos caminhos de uma pessoa: o espelho\n  de voz, a lista de quem está na sala, o registro de tempo em voz e a checagem\n  de admissão. Na fileira de rostos da sala o bot é um rosto como os outros, com\n  o anel de quem fala — essa fileira não desenha selo nenhum, de propósito; o\n  selo BOT aparece na lista de membros, no cartão e nas mensagens. Expulsar\n  tira da sala na hora (o servidor de mídia é cortado antes de qualquer aviso).\n  Se o programa do bot cair sem se despedir, quem o tira da sala é o tempo de\n  espera do próprio servidor de mídia — não é uma garantia da Gamerfy.\n\n### Ouvir a sala\n\nUm bot com a permissão **Ouvir a sala** — que só um administrador do servidor\nconcede, e que pode ser tirada sala por sala — entra ouvindo:\n\n```bash\ncurl -X POST 'https://api.gamerfy.gg/v1/channels/1834021894356992010/voice/listen' \\\n  -H 'Authorization: Bot gfb_…'\n```\n\n- O 201 traz `{ session_id, url, token }` e o mesmo `Location` da entrada WHIP.\n  O `url` e o `token` são do SDK: com eles o `voice.listen()` conecta, publica o\n  microfone do bot e recebe **a voz de cada pessoa separada**, já com o id da\n  conta de quem fala. O áudio da tela (jogo, vídeo) nunca é entregue.\n- Exige Ver canais, Conectar, Falar e Ouvir a sala (`403 missing_permission`). A\n  permissão de Administrador **não** inclui ouvir: só vale se for marcada.\n- **O bot aparece marcado como \"ouvindo\"**, e quem está na sala é avisado por\n  escrito. A marca vem do Gamerfy, não do bot.\n- Perder Ouvir a sala no meio da chamada corta a escuta **na hora**; o bot fica\n  na sala, falando.\n- Mesmas regras da entrada WHIP: uma sala por servidor, 6 entradas por minuto\n  na mesma sala (somando as duas entradas) e `DELETE` no `Location` para sair.\n- O que o bot faz com o que ouve — transcrever, guardar — é dele; o Gamerfy não\n  recebe nem guarda nada da conversa.\n- **Compartilhar a tela** (desde 06/11/2026): com a permissão **Transmitir tela e\n  câmera** — a mesma das pessoas —, o bot que entrou ouvindo também pode mostrar\n  uma imagem como tela compartilhada (`room.shareScreen()` no SDK). A sala recebe\n  como recebe a tela de qualquer pessoa: um convite para assistir, e só vê quem\n  abrir. Câmera e áudio da tela continuam fora para bots. Sem a permissão, o\n  servidor de mídia recusa a tela e o bot continua na sala, ouvindo e falando.\n\n### O cargo escolhido para o bot\n\nUm bot pode pedir um cargo ao ser adicionado (o campo **Pergunta do cargo** na\naba Desenvolvedor, como \"Quem pode chamar o Troia?\"). Quem adiciona escolhe um\ndos cargos do servidor — o @everyone vale para todos — e pode trocar depois nas\npermissões do bot. O bot pergunta na hora de decidir:\n\n```bash\ncurl 'https://api.gamerfy.gg/v1/guilds/1834021894356992000/members/1834021894356992111' \\\n  -H 'Authorization: Bot gfb_…'\n```\n\n- Responde `{ data: [membro] }` com `role_ids` e `holds_chosen_role`: `true` ou\n  `false`, e `null` quando o bot não pede cargo. `@me` no lugar do id lê o\n  próprio bot.\n- Servidor sem o bot: `404 guild_not_found`; conta que não é membro:\n  `404 member_not_found`.\n\n### Mensagem privada\n\nUm bot pode escrever para **uma pessoa**, numa conversa privada — para entregar\no que só ela deve ver, como um link de uso único:\n\n```bash\ncurl -X POST 'https://api.gamerfy.gg/v1/users/1834021894356992111/messages' \\\n  -H 'Authorization: Bot gfb_…' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"content\":\"Seu link: https://exemplo.com/mcp/abc\"}'\n# → 201 { \"data\": [ { \"id\": \"…\", \"channel_id\": \"…\", \"recipient_id\": \"1834021894356992111\",\n#                     \"author\": { …, \"bot\": true }, \"content\": \"Seu link: …\", \"created_at\": \"…\" } ] }\n```\n\n- A conversa abre na primeira mensagem e é a mesma nas seguintes (`channel_id`).\n  A pessoa a vê na caixa de mensagens dela, com o selo BOT, contada como não lida.\n- **Só para quem divide um servidor com o bot**: o bot precisa estar em pelo menos\n  um servidor em que a pessoa também está — senão `403 no_shared_guild`, checado\n  antes de tudo o que é da pessoa.\n- **A pessoa pode desligar**: \"Mensagens privadas de bots\", em Configurações →\n  Conta e perfil → Privacidade, vem ligado; desligado, `403 bot_dms_disabled`.\n- Bloqueio em qualquer sentido: `403 blocked`. Conta de bot (o próprio inclusive):\n  `400 bot_account`. Id de ninguém: `404 user_not_found`.\n- **É de mão única.** A pessoa lê, mas não responde — no lugar da caixa de\n  escrever, o app mostra \"Este bot não lê respostas.\" —, e o bot não recebe nada\n  da conversa: nenhum evento do gateway fala dela.\n- O corpo é o de `POST /v1/channels/{channel_id}/messages`: só `content`, até 2000\n  caracteres, sem anexos.\n- Até **20 mensagens privadas por minuto**, somando todas as pessoas. Uma recusa\n  não gasta vaga. No SDK é `bot.users.send(userId, content)`.\n\n### SDK\n\nO `@gamerfy/bot` fala tudo isso por você em Node 22 (mensagens, memória dos\nservidores, reconexão, WHIP, Ogg Opus e `ffmpeg`), e valida com zod tudo o que\nchega antes de qualquer callback seu. **Ainda não está publicado no npm**; hoje\nele mora em `packages/bot-sdk` no repositório do Gamerfy, com o guia completo no\n`README.md` de lá.\n\nO DJ — o exemplo `examples/dj.ts`, aqui sem o tratamento de erro que o arquivo\ntraz:\n\n```ts\nimport { Bot } from '@gamerfy/bot';\n\nconst bot = new Bot({ token: process.env.GAMERFY_BOT_TOKEN! });\n\nbot.on('message', async (message) => {\n  // `content` is null when the bot has no Ler mensagens and was not mentioned.\n  if (!message.mentionsMe || !message.content?.includes('tocar')) return;\n  const channelId = bot.voiceChannelOf(message.guildId, message.author.id);\n  if (channelId === null) {\n    await bot.messages.send(message.channelId, 'Entre numa sala de voz primeiro.');\n    return;\n  }\n  await bot.messages.send(message.channelId, 'Tocando lofi');\n  const voice = await bot.voice.join(channelId);\n  try {\n    await voice.play('./lofi.ogg');\n  } finally {\n    await voice.leave();\n  }\n});\n\nbot.on('error', (error) => console.error('[dj]', error.message));\nawait bot.connect();\n```\n\nNa mão, o mesmo bot é: nenhum `POST /oauth2/token` (o token de bot já é o\ntoken), um WebSocket com `identify` e `heartbeat`, e a oferta WHIP acima.\n\n## Vindo da Twitch\n\n| Na Twitch (Helix) | No Gamerfy | Diferença |\n|---|---|---|\n| `POST id.twitch.tv/oauth2/token` | `POST api.gamerfy.gg/oauth2/token` | mesmo corpo; token `gfa_…` de 60 dias |\n| `GET helix/users?login=` | `GET /v1/users?login=` | `links[]` já traz o `account_id` do PUBG |\n| `GET helix/streams?game_id=493057` | `GET /v1/streams?game_id=pubg` | `game_id` é slug; `recording_id` aponta para o replay futuro |\n| `GET helix/videos?user_id=&type=archive` | `GET /v1/videos?user_id=&type=archive` | `created_at` é o primeiro quadro, sem recuo; há `duration_seconds` |\n| `GET helix/games?id=` | `GET /v1/games?id=pubg` | devolve `twitch_category_id` para cruzar |\n| `GET helix/clips?broadcaster_id=` | `GET /v1/clips?broadcaster_id=` | `duration` em segundos, `vod_offset` sempre presente |\n| Player embed `?t=1h2m3s` | `gamerfy.gg/videos/{id}?t=1h2m3s` | mesmo formato de `t`; sem embed por iframe ainda |\n| EventSub | `POST /v1/webhooks` | sem *challenge*; assinatura HMAC no molde `id.timestamp.body` |\n| `id.twitch.tv/oauth2/authorize` (código) | `gamerfy.gg/autorizar` | PKCE `S256` obrigatório; `gfp_…` de 4 h e `gfr_…` que troca a cada renovação |\n| `POST`, `PATCH` e `GET helix/polls` | `POST`, `PATCH` e `GET /v1/polls` | com a chave do streamer (`gfp_…`, `polls:manage`/`polls:read`); `choices[].title` igual, `duration` em segundos; a resposta é a enquete dos eventos (`poll_id`, `choices[].votes`, `close_reason`), não o `status` da Helix |\n| `POST` e `DELETE helix/raids` | `POST` e `DELETE /v1/raids` | com a chave do streamer (`gfp_…`, `raids:manage`); o corpo é JSON (`from_broadcaster_id`, `to_broadcaster_id`), não query; a resposta traz `ends_at` e `waiting_answer` em vez de `is_mature` |\n| EventSub por WebSocket | `wss://api.gamerfy.gg/v1/events` | o protocolo do gateway dos bots, com retomada; os tipos seguem os nomes da EventSub (`channel.follow`, `channel.subscribe`, `channel.chat.message`…) |\n| `Ratelimit-*` (800/min) | os mesmos três cabeçalhos, 800/min por app | janela fixa de 60 s |\n| `view_count` (total de reproduções) | `view_count` em `/v1/videos` e `/v1/clips` | **pessoas distintas por dia, somadas dia a dia**: a mesma pessoa conta no máximo uma vez por dia em cada item, e reproduções repetidas no mesmo dia não somam. Não é o total de reproduções da Twitch. Atualizado a cada 10 minutos |\n\n## O que ainda não existe\n\n- **Challenge na inscrição do webhook**: a assinatura de cada entrega é a única\n  verificação. Também não há rota para disparar um evento de teste.\n- **Criar clipes pela API**: só pelo app; a API lista.\n- **Lives de canal de servidor**: só lives pessoais entram em `/v1/streams`,\n  são gravadas e disparam webhooks.\n- **Embed por iframe** das páginas de replay e de clipe (`embed_url` é `null`).\n- **`before` na paginação** e busca por título.\n- **Nos bots:** retomada entre nós do backend, comandos com barra, resposta a uma\n  mensagem privada de bot (a pessoa só lê), ICE trickle na entrada de voz, bot que **ouve** a sala, e SDK em outra linguagem além do\n  TypeScript.\n\n## English summary\n\nFor the pubg.report team.\n\nBase URL: `https://api.gamerfy.gg`. Create an app in the Gamerfy app\n(Configurações → Desenvolvedor) for a `client_id` + `client_secret` (shown once);\n`POST /oauth2/token` with `grant_type=client_credentials` returns a `gfa_…` bearer\ntoken (60 days). Twitch-shaped endpoints (`{ data, pagination: { cursor } }`,\n`snake_case`, ISO 8601 UTC): `GET /v1/users`, `/v1/streams` (lives on air),\n`/v1/videos` (replays, with `thumbnail_url`), `/v1/clips` (by `id`,\n`broadcaster_id` or `video_id`) and `/v1/games`. Public pages:\n`gamerfy.gg/videos/{id}?t=1h2m3s` and `gamerfy.gg/clips/{id}`. Match event → replay:\n`offset = event_time_utc − video.created_at`, no lead correction (`created_at` is the\nfirst recorded frame, ms precision). Webhooks: `POST /v1/webhooks` (up to 5 per app) for\n`stream.online`, `stream.offline`, `video.ready`, `clip.ready`, `channel.update`, HMAC-SHA256\nsigned; the live's own events (`channel.follow`, `channel.subscribe`, `channel.gift`,\n`channel.goal.*`, `channel.poll.*`…) reach an app only from the streamers who authorized it; a gift\ncarries its `message` only once it passed the filter (`message_state`), and `channel.gift.message_update`\nsays when the streamer let a held one through or refused it.\nUser authorization: OAuth 2 authorization code with PKCE (S256 required) at\n`gamerfy.gg/autorizar`, exchanged at `POST /oauth2/token` for a `gfp_…` (4 h) and a rotating\n`gfr_…` (60 days); scopes like `chat:read`, `gifts:read`. Event channel:\n`wss://api.gamerfy.gg/v1/events`, the bots' gateway protocol with a `gfp_…` key — chat,\nfollows, subscriptions, gifts and title changes as they happen, with resume. Overlays for OBS are\nplain web pages on that channel: the streamer's overlay key (`gfo_…`, read-only, kept after the `#` of\nthe page's address) and `identify` with `overlay: \"alerts\"` (or `chat`, `gift_qr`, `goal`, `poll`,\n`gift_rank`) bring the saved look in `snapshot.settings`; `overlay.alert` plays what the streamer's\nalert queue decided, and `overlay.settings` changes the look live.\nPolls: with the streamer's `gfp_…` and `polls:manage`, `POST /v1/polls` (`broadcaster_id`, `title`,\n`choices[{title}]`, `duration` 15–1800 s, optional `eligibility`) opens one on the air, `PATCH /v1/polls`\n(`status: \"TERMINATED\"`) ends it, and `GET /v1/polls?broadcaster_id=` (`polls:read`) reads them —\n`{ data: [...] }`, each item the same poll the `channel.poll.*` events carry; viewers vote at the top of\nthe chat, one vote per account.\nChat: with `chat:write`, `POST /v1/chat/messages` (`broadcaster_id`, `message` up to 500 chars) writes a line in\nthe live's chat as the app's own bot account (turn the bot on in the developer console; `409 app_has_no_bot`\notherwise), 20 lines per 30 s per app and chat; with `chat:moderate`,\n`DELETE /v1/chat/messages?broadcaster_id=&message_id=` deletes one line of that chat (204). The app's lines come\nback on the event channel as `channel.chat.message` with `badges: [\"bot\"]`.\nLimits: 800 requests/min per app, 100 ids per call; replays kept 14 days.\nOpenAPI 3.1: `https://api.gamerfy.gg/openapi.json`.\n","contact":{"name":"Gamerfy","url":"https://gamerfy.gg"}},"servers":[{"url":"https://api.gamerfy.gg","description":"Produção"}],"security":[{"appToken":[]}],"tags":[{"name":"Autenticação","description":"O token de app: `client_credentials`, 60 dias, enviado como `Authorization: Bearer gfa_…` em todas as rotas `/v1/*`. E as chaves de um streamer que autorizou o app em `gamerfy.gg/autorizar` (`gfp_…` por 4 horas, renovada pela `gfr_…`), por código com PKCE."},{"name":"Usuários","description":"Contas do Gamerfy e as contas de jogo que cada uma vinculou."},{"name":"Lives","description":"Lives pessoais no ar agora."},{"name":"Replays","description":"As lives gravadas, por 14 dias."},{"name":"Clipes","description":"Trechos de 5 a 60 segundos cortados de replays e lives."},{"name":"Jogos","description":"O catálogo, com os ids da Twitch e da Steam."},{"name":"Webhooks","description":"Em vez de consultar de minuto em minuto: o Gamerfy faz um `POST` assinado na sua URL a cada evento."},{"name":"Enquetes","description":"As enquetes da live de quem autorizou o app (`polls:manage` para abrir e encerrar, `polls:read` para ler), com a chave do streamer."},{"name":"Raids","description":"O raid da live de quem autorizou o app: mandar e cancelar (`raids:manage`), com a chave do streamer. Saber dos raids mandados e recebidos é pelo canal de eventos e pelos webhooks (`raids:read`)."},{"name":"Chat","description":"O chat da live de quem autorizou o app: escrever uma linha com o bot do app (`chat:write`) e apagar uma linha (`chat:moderate`), com a chave do streamer. Ler é pelo canal de eventos (`chat:read`)."},{"name":"Bots","description":"Rotas que um bot chama com o próprio token (`Authorization: Bot gfb_…`). O gateway em tempo real é servido em `/v1/gateway` (`wss://api.gamerfy.gg/v1/gateway`): como não é uma rota HTTP, os quadros, os eventos e os códigos de fechamento dele estão na seção **Bots** do guia, na introdução."}],"paths":{"/oauth2/token":{"post":{"operationId":"createToken","tags":["Autenticação"],"summary":"Obter o token","description":"Troca o `client_id` e o `client_secret` do app por um token de 60 dias (`client_credentials`, como na Twitch). O corpo pode ser JSON ou `application/x-www-form-urlencoded`.\n\nGuarde o token e reutilize até expirar: pedir um novo a cada chamada é desperdício.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"O token do app (`client_credentials`) ou as chaves de um streamer (`authorization_code`, `refresh_token`).","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Token"},{"$ref":"#/components/schemas/UserToken"}]},"example":{"access_token":"gfa_4kQ9mB2xT7pL0vN8rY3wZ6cH1jF5dS9aK2eU4oI7","expires_in":5184000,"token_type":"bearer"}}}},"400":{"description":"`invalid_body` (corpo malformado ou campo faltando) ou `unsupported_grant_type`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unsupported_grant_type":{"value":{"error":"unsupported_grant_type","message":"apenas grant_type=client_credentials é aceito"}},"invalid_body":{"value":{"error":"invalid_body","message":"corpo da requisição inválido","details":[{"path":"client_secret","message":"Invalid input: expected string, received undefined"}]}}}}}},"401":{"description":"`client_id` ou `client_secret` errados (a mesma mensagem nos dois casos).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_client","message":"client id ou client secret inválidos"}}}}}}},"/oauth2/revoke":{"post":{"operationId":"revokeToken","tags":["Autenticação"],"summary":"Revogar uma chave de usuário","description":"Tira uma chave de um streamer (RFC 7009): uma `gfp_` sai sozinha; uma `gfr_` tira a autorização inteira e fecha as conexões do app no canal de eventos. Uma chave que não existe recebe o mesmo 200.","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["client_id","token"],"properties":{"client_id":{"type":"string","maxLength":64},"token":{"type":"string","maxLength":128},"token_type_hint":{"type":"string","maxLength":32}}}}}},"responses":{"200":{"description":"Revogada (ou nunca existiu)."},"400":{"description":"`invalid_request`: faltou `client_id` ou `token`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_request","message":"token: Invalid input"}}}},"401":{"description":"`invalid_client`: app desconhecido ou com a autorização desligada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_client","message":"app desconhecido ou com a autorização desligada"}}}}}}},"/oauth2/validate":{"get":{"operationId":"validateToken","tags":["Autenticação"],"summary":"Validar o token","description":"Diz de qual app é o token e quantos segundos faltam para expirar. Com uma chave de streamer (`gfp_`), diz também de quem é a live e quais permissões ela tem.","security":[{"appToken":[]}],"responses":{"200":{"description":"O app e o tempo restante.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/TokenValidation"},{"$ref":"#/components/schemas/UserTokenValidation"}]},"example":{"client_id":"k3v9x2m7q1w8e4r6t0y5u3i9o2p7a1","app_name":"pubg.report","expires_in":5180000}}}},"401":{"$ref":"#/components/responses/InvalidToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/users":{"get":{"operationId":"getUsers","tags":["Usuários"],"summary":"Usuários","description":"Usuários por id ou por login. Pelo menos um dos dois; juntos somam até 100. Um id ou login que não existe simplesmente não aparece em `data`. A lista sai da conta mais antiga para a mais nova.","security":[{"appToken":[]}],"parameters":[{"name":"id","in":"query","description":"Id do usuário. Repetível: `?id=a&id=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["1834021894356992001"]},{"name":"login","in":"query","description":"Username, sem distinguir maiúsculas. Repetível: `?login=a&login=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["henrique"]}],"responses":{"200":{"description":"Os usuários encontrados.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsersResponse"},"example":{"data":[{"id":"1834021894356992001","login":"henrique","display_name":"Henrique","profile_image_url":"https://gamerfy.gg/api/users/1834021894356992001/avatar?v=3f9a1c2b","created_at":"2026-09-01T12:00:00.000Z","links":[{"game_id":"pubg","account_id":"account.c0e530e9b7244b358def282782f893de","account_name":"HenriqueTJ"}]}]}}}},"400":{"$ref":"#/components/responses/InvalidQuery"},"401":{"$ref":"#/components/responses/InvalidToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/streams":{"get":{"operationId":"getStreams","tags":["Lives"],"summary":"Lives no ar","description":"Lives **pessoais** no ar agora, das mais recentes para as mais antigas. Sem filtro, lista todas.","security":[{"appToken":[]}],"parameters":[{"name":"user_id","in":"query","description":"Id do streamer. Repetível: `?user_id=a&user_id=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["1834021894356992001"]},{"name":"user_login","in":"query","description":"Login do streamer. Repetível: `?user_login=a&user_login=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["henrique"]},{"name":"game_id","in":"query","description":"Slug do jogo. Repetível: `?game_id=a&game_id=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["pubg"]},{"name":"first","in":"query","description":"Tamanho da página, de 1 a 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","description":"O `pagination.cursor` da página anterior. Opaco: não monte nem edite.","schema":{"type":"string"}}],"responses":{"200":{"description":"Uma página de lives.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamsResponse"},"example":{"data":[{"id":"1834100000000000042","user_id":"1834021894356992001","user_login":"henrique","user_name":"Henrique","game_id":"pubg","title":"Ranked de terça","viewer_count":37,"started_at":"2026-09-08T21:30:00.123Z","type":"live","recording_id":"1834100000000000042"}],"pagination":{"cursor":"eyJzdGFydGVkX2F0Ijoi"}}}}},"400":{"$ref":"#/components/responses/InvalidPageQuery"},"401":{"$ref":"#/components/responses/InvalidToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/videos":{"get":{"operationId":"getVideos","tags":["Replays"],"summary":"Replays","description":"Replays das lives pessoais: só os prontos, públicos e dentro da retenção (14 dias). Pelo menos **um** filtro entre `id`, `user_id` e `game_id`; mais de um restringe (E, não OU). Ordem: `created_at` decrescente.\n\nUm replay truncado por falta de disco ainda aparece; a duração é a do que foi gravado.","security":[{"appToken":[]}],"parameters":[{"name":"id","in":"query","description":"Id do replay. Repetível: `?id=a&id=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["1834100000000000042"]},{"name":"user_id","in":"query","description":"Id do streamer (um só).","schema":{"type":"string","pattern":"^\\d{1,19}$"}},{"name":"game_id","in":"query","description":"Slug do jogo.","schema":{"type":"string"}},{"name":"type","in":"query","description":"`all` hoje significa o mesmo que `archive`.","schema":{"type":"string","enum":["archive","all"],"default":"archive"}},{"name":"first","in":"query","description":"Tamanho da página, de 1 a 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","description":"O `pagination.cursor` da página anterior. Opaco: não monte nem edite.","schema":{"type":"string"}}],"responses":{"200":{"description":"Uma página de replays.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideosResponse"},"example":{"data":[{"id":"1834100000000000042","user_id":"1834021894356992001","user_login":"henrique","user_name":"Henrique","title":"Ranked de terça","created_at":"2026-09-08T21:30:00.123Z","published_at":"2026-09-08T23:44:09.870Z","duration":"2h14m9s","duration_seconds":8049,"url":"https://gamerfy.gg/videos/1834100000000000042","game_id":"pubg","type":"archive","viewable":"public","thumbnail_url":"https://vod.gamerfy.gg/bcdn_token=Xq3v9ZtL2kWm8RpA&token_path=%2Fvods%2F1834100000000000042%2F&expires=1789000000/vods/1834100000000000042/thumbnail.jpg","view_count":1840}],"pagination":{"cursor":"eyJzdGFydGVkX2F0Ijoi"}}}}},"400":{"$ref":"#/components/responses/InvalidPageQuery"},"401":{"$ref":"#/components/responses/InvalidToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/clips":{"get":{"operationId":"getClips","tags":["Clipes"],"summary":"Clipes","description":"Clipes de 5 a 60 segundos cortados de um replay ou de uma live no ar. Só os **prontos e públicos**. Pelo menos **um** filtro entre `id`, `broadcaster_id` e `video_id`; mais de um restringe (E, não OU). Ordem: `created_at` decrescente.\n\nUm clipe de replay privado nunca aparece, nem depois de o streamer tornar o replay público.","security":[{"appToken":[]}],"parameters":[{"name":"id","in":"query","description":"Id do clipe. Repetível: `?id=a&id=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["1834200000000000077"]},{"name":"broadcaster_id","in":"query","description":"Id do streamer.","schema":{"type":"string","pattern":"^\\d{1,19}$"}},{"name":"video_id","in":"query","description":"Id do replay de origem.","schema":{"type":"string","pattern":"^\\d{1,19}$"}},{"name":"started_at","in":"query","description":"ISO 8601 (`Z` ou fuso): só clipes criados neste instante ou depois.","schema":{"type":"string","format":"date-time"}},{"name":"ended_at","in":"query","description":"ISO 8601: só clipes criados neste instante ou antes. Igual ou posterior a `started_at`.","schema":{"type":"string","format":"date-time"}},{"name":"first","in":"query","description":"Tamanho da página, de 1 a 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","description":"O `pagination.cursor` da página anterior. Opaco: não monte nem edite.","schema":{"type":"string"}}],"responses":{"200":{"description":"Uma página de clipes.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClipsResponse"},"example":{"data":[{"id":"1834200000000000077","url":"https://gamerfy.gg/clips/1834200000000000077","embed_url":null,"broadcaster_id":"1834021894356992001","broadcaster_name":"Henrique","creator_id":"1834021894356992002","creator_name":"Fulano","video_id":"1834100000000000042","game_id":"pubg","title":"Headshot do último círculo","view_count":12400,"created_at":"2026-09-09T13:21:44.512Z","thumbnail_url":"https://vod.gamerfy.gg/bcdn_token=Xq3v9ZtL2kWm8RpA&token_path=%2Fclips%2F1834200000000000077%2F&expires=1789000000/clips/1834200000000000077/thumbnail.jpg","duration":30,"vod_offset":4415}],"pagination":{}}}}},"400":{"$ref":"#/components/responses/InvalidPageQuery"},"401":{"$ref":"#/components/responses/InvalidToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/games":{"get":{"operationId":"getGames","tags":["Jogos"],"summary":"Jogos","description":"Os jogos que o Gamerfy conhece, com os ids da Twitch e da Steam para cruzar catálogos. Sem parâmetros, lista todos. `id` e `name` não distinguem maiúsculas e somam (OU).","security":[{"appToken":[]}],"parameters":[{"name":"id","in":"query","description":"Slug do jogo. Repetível: `?id=a&id=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["pubg"]},{"name":"name","in":"query","description":"Nome exato. Repetível: `?name=a&name=b`.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true,"example":["PUBG: Battlegrounds"]}],"responses":{"200":{"description":"Os jogos.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GamesResponse"},"example":{"data":[{"id":"pubg","name":"PUBG: Battlegrounds","twitch_category_id":"493057","steam_app_id":578080}]}}}},"400":{"$ref":"#/components/responses/InvalidQuery"},"401":{"$ref":"#/components/responses/InvalidToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/webhooks":{"post":{"operationId":"createWebhook","tags":["Webhooks"],"summary":"Registrar um webhook","description":"Registra uma URL que recebe um `POST` assinado a cada evento. Até **5 webhooks por app**. O `secret` aparece só nesta resposta: se perder, apague e registre de novo.","security":[{"appToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookRequest"},"example":{"url":"https://hooks.pubg.report/gamerfy","events":["stream.online","stream.offline","video.ready","clip.ready","channel.update","channel.follow","channel.subscribe","channel.subscription.end","channel.gift","channel.gift.message_update","channel.raid","channel.goal.begin","channel.goal.progress","channel.goal.achieved","channel.goal.end","channel.poll.begin","channel.poll.progress","channel.poll.end"]}}}},"responses":{"201":{"description":"Registrado.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreatedResponse"},"example":{"data":[{"id":"1834300000000000005","url":"https://hooks.pubg.report/gamerfy","events":["stream.online","stream.offline","video.ready","clip.ready","channel.update","channel.follow","channel.subscribe","channel.subscription.end","channel.gift","channel.gift.message_update","channel.raid","channel.goal.begin","channel.goal.progress","channel.goal.achieved","channel.goal.end","channel.poll.begin","channel.poll.progress","channel.poll.end"],"secret":"u3Zk8sQ1pV6nW2yR4tE7oA9cL5mB0xH3jG1fD8kS2qN","created_at":"2026-09-09T14:00:00.000Z"}]}}}},"400":{"description":"URL fora das regras, evento desconhecido ou lista vazia; `details` diz qual.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_body","message":"corpo da requisição inválido"}}}},"401":{"$ref":"#/components/responses/InvalidToken"},"409":{"description":"O app já tem o máximo de webhooks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"webhook_limit_reached","message":"um app pode ter no máximo 5 webhooks"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"operationId":"listWebhooks","tags":["Webhooks"],"summary":"Listar webhooks","description":"Os webhooks do próprio app, do mais antigo para o mais novo, sem o segredo.","security":[{"appToken":[]}],"responses":{"200":{"description":"Os webhooks.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhooksResponse"},"example":{"data":[{"id":"1834300000000000005","url":"https://hooks.pubg.report/gamerfy","events":["stream.online","stream.offline","video.ready","clip.ready","channel.update","channel.follow","channel.subscribe","channel.subscription.end","channel.gift","channel.gift.message_update","channel.raid","channel.goal.begin","channel.goal.progress","channel.goal.achieved","channel.goal.end","channel.poll.begin","channel.poll.progress","channel.poll.end"],"failures":0,"disabled_at":null,"created_at":"2026-09-09T14:00:00.000Z"}]}}}},"401":{"$ref":"#/components/responses/InvalidToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/polls":{"get":{"operationId":"getPolls","tags":["Enquetes"],"summary":"Enquetes","description":"A enquete aberta e as dez anteriores do streamer que autorizou o app, ou uma só pelo `id`. Com a chave dele (`gfp_…`) e `polls:read`; `broadcaster_id` tem de ser o dono da chave.","security":[{"userToken":["polls:read"]}],"parameters":[{"name":"broadcaster_id","in":"query","description":"O streamer que autorizou o app.","schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do streamer.","examples":["1834021894356992001"]},"required":true},{"name":"id","in":"query","description":"Uma enquete só.","schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da enquete.","examples":["1834021894356992001"]}}],"responses":{"200":{"description":"As enquetes.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollsResponse"},"example":{"data":[{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","poll_id":"1834400000000000099","title":"Qual mapa agora?","eligibility":"everyone","choices":[{"id":"a","title":"Mirage","votes":41},{"id":"b","title":"Inferno","votes":26}],"total_votes":67,"started_at":"2026-09-29T22:00:00.000Z","ends_at":"2026-09-29T22:03:00.000Z","ended_at":null,"close_reason":null}]}}}},"400":{"$ref":"#/components/responses/InvalidQuery"},"401":{"description":"Chave de usuário ausente, inválida ou expirada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_token","message":"token de usuário ausente, inválido ou expirado"}}}},"403":{"description":"A chave não tem `polls:read` (`details.scope`), ou `broadcaster_id` é de outro streamer (`broadcaster_mismatch`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_scope","message":"a autorização deste app não inclui a permissão pedida"}}}},"404":{"description":"O `id` não é de uma enquete desse streamer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"poll_not_found","message":"enquete não encontrada"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"As enquetes ainda não estão ligadas neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"live_interactive_disabled","message":"este recurso da live ainda não está disponível"}}}}}},"post":{"operationId":"createPoll","tags":["Enquetes"],"summary":"Abrir uma enquete","description":"Abre uma enquete na live do streamer, como o painel dele abriria: com a live no ar, uma por vez, 2 a 5 opções, de 15 segundos a 30 minutos. Um voto por conta, contado no servidor; quem assiste vota no topo do chat. Com `polls:manage`.","security":[{"userToken":["polls:manage"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollCreateRequest"},"example":{"broadcaster_id":"1834021894356992001","title":"Qual mapa agora?","choices":[{"title":"Mirage"},{"title":"Inferno"}],"duration":180}}}},"responses":{"201":{"description":"Aberta.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollsResponse"},"example":{"data":[{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","poll_id":"1834400000000000099","title":"Qual mapa agora?","eligibility":"everyone","choices":[{"id":"1","title":"Mirage","votes":0},{"id":"2","title":"Inferno","votes":0}],"total_votes":0,"started_at":"2026-09-29T22:00:00.000Z","ends_at":"2026-09-29T22:03:00.000Z","ended_at":null,"close_reason":null}]}}}},"400":{"description":"Um campo fora das regras: `poll_question_invalid`, `poll_options_invalid`, `poll_option_invalid`, `poll_option_repeated`, `poll_duration_invalid` ou `invalid_body`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"poll_options_invalid","message":"uma enquete tem de 2 a 5 opções"}}}},"401":{"description":"Chave de usuário ausente, inválida ou expirada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_token","message":"token de usuário ausente, inválido ou expirado"}}}},"403":{"description":"A chave não tem `polls:manage` (`details.scope`), ou `broadcaster_id` é de outro streamer (`broadcaster_mismatch`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_scope","message":"a autorização deste app não inclui a permissão pedida"}}}},"409":{"description":"Já há uma enquete aberta (`poll_open`), ou a live está fora do ar (`creator_not_live`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"poll_open","message":"já existe uma enquete aberta; encerre-a para abrir outra"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"As enquetes ainda não estão ligadas neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"live_interactive_disabled","message":"este recurso da live ainda não está disponível"}}}}}},"patch":{"operationId":"endPoll","tags":["Enquetes"],"summary":"Encerrar uma enquete","description":"Encerra a enquete agora (`status: \"TERMINATED\"`), com o resultado contado no servidor. Com `polls:manage`.","security":[{"userToken":["polls:manage"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollEndRequest"},"example":{"broadcaster_id":"1834021894356992001","id":"1834400000000000099","status":"TERMINATED"}}}},"responses":{"200":{"description":"Encerrada.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollsResponse"},"example":{"data":[{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","poll_id":"1834400000000000099","title":"Qual mapa agora?","eligibility":"everyone","choices":[{"id":"a","title":"Mirage","votes":41},{"id":"b","title":"Inferno","votes":26}],"total_votes":67,"started_at":"2026-09-29T22:00:00.000Z","ends_at":"2026-09-29T22:03:00.000Z","ended_at":"2026-09-29T22:01:30.000Z","close_reason":"ended"}]}}}},"400":{"description":"Corpo fora das regras.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_body","message":"corpo da requisição inválido"}}}},"401":{"description":"Chave de usuário ausente, inválida ou expirada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_token","message":"token de usuário ausente, inválido ou expirado"}}}},"403":{"description":"A chave não tem `polls:manage` (`details.scope`), ou `broadcaster_id` é de outro streamer (`broadcaster_mismatch`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"broadcaster_mismatch","message":"`broadcaster_id` não é o streamer que autorizou este app"}}}},"404":{"description":"O `id` não é de uma enquete desse streamer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"poll_not_found","message":"enquete não encontrada"}}}},"409":{"description":"A enquete já tinha fechado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"poll_closed","message":"esta enquete já fechou"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"As enquetes ainda não estão ligadas neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"live_interactive_disabled","message":"este recurso da live ainda não está disponível"}}}}}}},"/v1/raids":{"post":{"operationId":"startRaid","tags":["Raids"],"summary":"Mandar um raid","description":"Começa um raid da live do streamer que autorizou o app para outra live, como o painel dele começaria: a contagem de 60 segundos aparece para todo mundo, e o raid sai sozinho no fim dela (ou depois do sim de quem pergunta a cada raid). As mesmas regras do painel: pelo menos 5 contas assistindo, conta com 7 dias ou mais, 1 hora para repetir o mesmo canal, no máximo 10 por dia, e quem recebe precisa aceitar raids de quem manda. Com `raids:manage`.","security":[{"userToken":["raids:manage"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RaidStartRequest"},"example":{"from_broadcaster_id":"1834021894356992001","to_broadcaster_id":"1834021894356992077"}}}},"responses":{"201":{"description":"A contagem começou.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RaidStartedResponse"},"example":{"data":[{"id":"1834400000000000120","created_at":"2026-10-08T22:00:00.000Z","ends_at":"2026-10-08T22:01:00.000Z","waiting_answer":false}]}}}},"400":{"description":"Corpo fora das regras.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_body","message":"corpo da requisição inválido"}}}},"401":{"description":"Chave de usuário ausente, inválida ou expirada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_token","message":"token de usuário ausente, inválido ou expirado"}}}},"403":{"description":"A chave não tem `raids:manage` (`details.scope`), `from_broadcaster_id` é de outro streamer (`broadcaster_mismatch`), a conta é nova demais (`raid_account_new`), há bloqueio entre os dois (`raid_blocked`) ou quem recebe não aceita raid desse canal (`raid_not_accepted`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"raid_not_accepted","message":"Esse canal não aceita raid seu."}}}},"409":{"description":"A live está fora do ar (`raid_not_live`), tem menos de 5 contas assistindo (`raid_too_few`), já há um raid em andamento (`raid_busy`), o destino está fora do ar (`raid_target_offline`), é o próprio streamer (`raid_self`) ou pausou os raids (`raid_target_paused`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"raid_target_offline","message":"Esse canal não está ao vivo agora."}}}},"429":{"description":"O limite do app, os 10 raids das últimas 24 horas (`raid_daily_limit`) ou a hora de espera para repetir o mesmo canal (`raid_same_target`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"raid_daily_limit","message":"Você chegou a 10 raids nas últimas 24 horas. Tente mais tarde."}}}},"503":{"description":"O raid ainda não está ligado neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"raid_disabled","message":"o raid ainda não está disponível"}}}}}},"delete":{"operationId":"cancelRaid","tags":["Raids"],"summary":"Cancelar o raid","description":"Cancela o raid em contagem do streamer que autorizou o app (ou o raid deixado para o fim da live). Ninguém é levado. Com `raids:manage`.","security":[{"userToken":["raids:manage"]}],"parameters":[{"name":"broadcaster_id","in":"query","description":"O streamer que autorizou o app.","schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do streamer.","examples":["1834021894356992001"]},"required":true}],"responses":{"204":{"description":"Cancelado.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}}},"400":{"description":"Falta `broadcaster_id`, ou veio um parâmetro que não existe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_query","message":"parâmetros inválidos"}}}},"401":{"description":"Chave de usuário ausente, inválida ou expirada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_token","message":"token de usuário ausente, inválido ou expirado"}}}},"403":{"description":"A chave não tem `raids:manage` (`details.scope`), ou `broadcaster_id` é de outro streamer (`broadcaster_mismatch`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_scope","message":"a autorização deste app não inclui a permissão pedida"}}}},"404":{"description":"Não há raid em andamento.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"raid_not_found","message":"Esse raid já terminou."}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"O raid ainda não está ligado neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"raid_disabled","message":"o raid ainda não está disponível"}}}}}}},"/v1/chat/messages":{"post":{"operationId":"sendChatMessage","tags":["Chat"],"summary":"Escrever no chat da live","description":"Escreve uma linha no chat da live do streamer que autorizou o app. Quem fala é **a conta de bot do próprio app** (Configurações → Desenvolvedor → Ligar bot): o chat mostra o nome do app com o selo BOT, nunca o streamer. A linha chega a todo mundo como qualquer outra — no app, no site, no overlay de chat — e ao canal de eventos como `channel.chat.message` com `badges: [\"bot\"]`. No máximo 20 linhas a cada 30 segundos por app e chat; uma linha recusada não gasta vaga. Com `chat:write`.","security":[{"userToken":["chat:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatMessageRequest"},"example":{"broadcaster_id":"1834021894356992001","message":"Lembrete: sem spoiler no chat!"}}}},"responses":{"200":{"description":"Escrita.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatMessageSentResponse"},"example":{"data":[{"message_id":"98765432109876543","is_sent":true}]}}}},"400":{"description":"Mensagem vazia, com mais de 500 caracteres ou começando com `/`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_body","message":"corpo da requisição inválido"}}}},"401":{"description":"Chave de usuário ausente, inválida ou expirada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_token","message":"token de usuário ausente, inválido ou expirado"}}}},"403":{"description":"A chave não tem `chat:write` (`details.scope`), `broadcaster_id` é de outro streamer (`broadcaster_mismatch`), ou o streamer bloqueou o bot (`blocked`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_scope","message":"a autorização deste app não inclui a permissão pedida"}}}},"404":{"description":"O streamer ainda não tem o chat da live (nunca abriu a área Live).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"chat_not_found","message":"este streamer ainda não tem o chat da live"}}}},"409":{"description":"O app não tem bot ligado: não há quem fale por ele.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"app_has_no_bot","message":"para escrever no chat o app precisa do bot ligado, em Configurações → Desenvolvedor"}}}},"429":{"description":"O limite do app (800 por minuto), ou as 20 linhas a cada 30 segundos deste chat — com a espera em `details.retry_after_ms` e em `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"rate_limited","message":"no máximo 20 mensagens a cada 30 segundos por chat"}}}},"503":{"description":"O chat para apps ainda não está ligado neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"live_interactive_disabled","message":"este recurso da live ainda não está disponível"}}}}}},"delete":{"operationId":"deleteChatMessage","tags":["Chat"],"summary":"Apagar uma linha do chat da live","description":"Apaga uma linha do chat da live do streamer que autorizou o app, como ele mesmo apagaria: ela some para todo mundo na hora, e o canal de eventos diz `channel.chat.message_delete`. Só linhas **desse chat**: o id de uma mensagem de outro lugar (um servidor, uma conversa, outra live) é o mesmo 404 de um id que não existe. Não há como limpar o chat inteiro: o `message_id` é obrigatório. Com `chat:moderate`.","security":[{"userToken":["chat:moderate"]}],"parameters":[{"name":"broadcaster_id","in":"query","description":"O streamer que autorizou o app.","schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do streamer.","examples":["1834021894356992001"]},"required":true},{"name":"message_id","in":"query","description":"A linha a apagar.","schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da linha: o `message_id` de `channel.chat.message`.","examples":["1834021894356992001"]},"required":true}],"responses":{"204":{"description":"Apagada.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}}},"400":{"description":"Falta `broadcaster_id` ou `message_id`, ou veio um parâmetro que não existe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_query","message":"parâmetros inválidos"}}}},"401":{"description":"Chave de usuário ausente, inválida ou expirada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_token","message":"token de usuário ausente, inválido ou expirado"}}}},"403":{"description":"A chave não tem `chat:moderate` (`details.scope`), ou `broadcaster_id` é de outro streamer (`broadcaster_mismatch`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_scope","message":"a autorização deste app não inclui a permissão pedida"}}}},"404":{"description":"A linha não existe, já foi apagada ou não é do chat dessa live (`message_not_found`); ou o streamer ainda não tem o chat (`chat_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"message_not_found","message":"mensagem não encontrada"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"O chat para apps ainda não está ligado neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"live_interactive_disabled","message":"este recurso da live ainda não está disponível"}}}}}}},"/v1/webhooks/{id}":{"delete":{"operationId":"deleteWebhook","tags":["Webhooks"],"summary":"Remover um webhook","description":"Remove um webhook do app. É também como se \"reativa\" um webhook desligado por falhas: apague e registre de novo.","security":[{"appToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Id do webhook.","schema":{"type":"string","pattern":"^\\d{1,19}$"}}],"responses":{"204":{"description":"Removido.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}}},"401":{"$ref":"#/components/responses/InvalidToken"},"404":{"description":"O id não é de um webhook do seu app.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"webhook_not_found","message":"webhook não encontrado"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/bot/@me":{"get":{"operationId":"getBotSelf","tags":["Bots"],"summary":"O próprio bot","security":[{"botToken":[]}],"responses":{"200":{"description":"A conta do bot.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotSelfResponse"},"example":{"data":[{"id":"1834021894356992001","username":"dj_gamer","display_name":"DJ Gamer","avatar_url":null,"bot":true,"app_id":"1834021894356992001"}]}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/bot/guilds":{"get":{"operationId":"getBotGuilds","tags":["Bots"],"summary":"Servidores do bot","description":"Os servidores em que o bot está, com as permissões dele em cada um. Até 100 servidores por bot nesta fase; a lista não pagina — vem inteira.","security":[{"botToken":[]}],"responses":{"200":{"description":"Os servidores.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotGuildsResponse"},"example":{"data":[{"id":"1834021894356992001","name":"Clã dos Amigos","icon_url":null,"permissions":"8963"}]}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/guilds/{guild_id}":{"get":{"operationId":"getBotGuild","tags":["Bots"],"summary":"O servidor como o bot o vê","description":"A concessão do bot, as salas e canais em que ele tem Ver canais e quem está em qual dessas salas de voz — a mesma forma que o `ready` do gateway entrega por servidor. Os `voice_states` são conferidos contra o LiveKit antes da resposta (no máximo uma vez a cada 5 s por servidor): é a leitura a fazer no instante de agir sobre a sala de alguém — um `/play` que entra na sala de quem pediu.","security":[{"botToken":[]}],"parameters":[{"name":"guild_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]}}],"responses":{"200":{"description":"O servidor.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotGuildResponse"},"example":{"data":[{"id":"1834021894356992001","name":"Clã dos Amigos","icon_url":null,"permissions":"8963","channels":[{"id":"1834021894356992001","guild_id":"1834021894356992001","type":"voice","name":"Jogando","position":0,"permissions":"8963"}],"voice_states":[{"user_id":"1834021894356992001","channel_id":"1834021894356992001"}]}]}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"404":{"description":"O bot não está neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"guild_not_found","message":"servidor não encontrado"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/guilds/{guild_id}/channels":{"get":{"operationId":"getBotGuildChannels","tags":["Bots"],"summary":"Salas que o bot enxerga","description":"Só as salas e canais em que o bot tem Ver canais, com as permissões dele em cada um.","security":[{"botToken":[]}],"parameters":[{"name":"guild_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]}}],"responses":{"200":{"description":"As salas.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotChannelsResponse"},"example":{"data":[{"id":"1834021894356992001","guild_id":"1834021894356992001","type":"voice","name":"Jogando","position":0,"permissions":"8963"}]}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"404":{"description":"O bot não está neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"guild_not_found","message":"servidor não encontrado"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/guilds/{guild_id}/members":{"get":{"operationId":"getBotGuildMembers","tags":["Bots"],"summary":"Membros do servidor","description":"Pagina como o resto desta API: `first` (1 a 100, 20 por omissão) e `after`, o cursor opaco da página anterior. Um `after` de OUTRA listagem deste `/v1` decodifica normalmente e devolve uma página qualquer, não um erro — o cursor é opaco por convenção, não assinado.","security":[{"botToken":[]}],"parameters":[{"name":"guild_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]}},{"name":"first","in":"query","description":"Tamanho da página, de 1 a 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"after","in":"query","description":"O `pagination.cursor` da página anterior. Opaco: não monte nem edite.","schema":{"type":"string"}}],"responses":{"200":{"description":"Uma página de membros.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotMembersResponse"},"example":{"data":[{"user":{"id":"1834021894356992001","username":"henrique","display_name":"Henrique","avatar_url":null,"bot":false},"nickname":null,"joined_at":"2026-09-15T12:00:00.000Z"}],"pagination":{}}}}},"400":{"$ref":"#/components/responses/InvalidPageQuery"},"401":{"$ref":"#/components/responses/BotInvalidToken"},"404":{"description":"O bot não está neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"guild_not_found","message":"servidor não encontrado"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/channels/{channel_id}/messages":{"post":{"operationId":"createBotMessage","tags":["Bots"],"summary":"Escrever uma mensagem","description":"Exige Ver canais e Enviar mensagens, nessa ordem — sem a primeira, a segunda nem chega a ser checada, e `details.permission` diz qual das duas faltou. Até **5 mensagens a cada 5 segundos por canal**, além do limite do app.","security":[{"botToken":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do canal.","examples":["1834021894356992001"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotMessageRequest"},"example":{"content":"Tocando lofi"}}}},"responses":{"201":{"description":"Escrita.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotMessageResponse"},"example":{"data":[{"id":"1834021894356992001","guild_id":"1834021894356992001","channel_id":"1834021894356992001","author":{"id":"1834021894356992001","username":"dj_gamer","display_name":"DJ Gamer","avatar_url":null,"bot":true},"content":"Tocando lofi","mentions":[],"attachments":[],"created_at":"2026-09-15T12:00:00.000Z","edited_at":null}]}}}},"400":{"description":"Texto vazio, maior que 2000 caracteres, ou com um campo que esta API não conhece (ex.: anexos — um bot desta fase não envia arquivos).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_body","message":"corpo da requisição inválido"}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"403":{"description":"Falta Ver canais ou Enviar mensagens.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_permission","message":"esta ação exige a permissão SEND_MESSAGES","details":{"permission":"SEND_MESSAGES"}}}}},"404":{"description":"Canal inexistente, de um servidor onde o bot não está, ou que não é de um servidor (uma conversa direta, por exemplo).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"channel_not_found","message":"canal não encontrado"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/channels/{channel_id}/messages/{message_id}":{"delete":{"operationId":"deleteBotMessage","tags":["Bots"],"summary":"Apagar uma mensagem","description":"A própria sempre (só precisa Ver canais); a de outra pessoa exige também Gerenciar mensagens.","security":[{"botToken":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do canal.","examples":["1834021894356992001"]}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da mensagem.","examples":["1834021894356992001"]}}],"responses":{"204":{"description":"Apagada."},"401":{"$ref":"#/components/responses/BotInvalidToken"},"403":{"description":"Falta uma permissão: Ver canais (checada primeiro) ou, para apagar a mensagem de outra pessoa, Gerenciar mensagens.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"view_channel":{"value":{"error":"missing_permission","message":"esta ação exige a permissão VIEW_CHANNEL","details":{"permission":"VIEW_CHANNEL"}}},"manage_messages":{"value":{"error":"missing_permission","message":"esta ação exige a permissão MANAGE_MESSAGES","details":{"permission":"MANAGE_MESSAGES"}}}}}}},"404":{"description":"Canal inexistente, de um servidor onde o bot não está, ou que não é de um servidor (`channel_not_found`); ou mensagem inexistente, já apagada, ou de outro canal (`message_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"channel_not_found":{"value":{"error":"channel_not_found","message":"canal não encontrado"}},"message_not_found":{"value":{"error":"message_not_found","message":"mensagem não encontrada"}}}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/users/{user_id}/messages":{"post":{"operationId":"createBotDirectMessage","tags":["Bots"],"summary":"Mandar uma mensagem privada","description":"Escreve para uma pessoa numa conversa privada — aberta na primeira mensagem, a mesma nas seguintes. Só para quem está em pelo menos um servidor junto com o bot, mantém ligado \"Mensagens privadas de bots\" (Configurações → Conta e perfil → Privacidade; vem ligado) e não tem bloqueio com o bot em nenhum dos sentidos. **É de mão única:** a pessoa lê, mas não responde, e o bot não recebe nada da conversa. Até **20 mensagens privadas por minuto**, somando todas as pessoas, além do limite do app; uma recusa não gasta vaga.","security":[{"botToken":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da pessoa.","examples":["1834021894356992001"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotMessageRequest"},"example":{"content":"Seu link: https://exemplo.com/mcp/abc"}}}},"responses":{"201":{"description":"Escrita.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotDirectMessageResponse"},"example":{"data":[{"id":"1834021894356992001","channel_id":"1834021894356992001","recipient_id":"1834021894356992001","author":{"id":"1834021894356992001","username":"troia","display_name":"Troia","avatar_url":null,"bot":true},"content":"Seu link: https://exemplo.com/mcp/abc","created_at":"2026-11-05T12:00:00.000Z"}]}}}},"400":{"description":"Texto vazio, maior que 2000 caracteres ou com um campo que esta API não conhece (`invalid_body`); ou a conta é de um bot — o próprio inclusive —, e bot não recebe mensagem privada (`bot_account`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_body":{"value":{"error":"invalid_body","message":"corpo da requisição inválido"}},"bot_account":{"value":{"error":"bot_account","message":"bots não recebem mensagens diretas"}}}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"403":{"description":"A pessoa não está em nenhum servidor com o bot (`no_shared_guild`, checado primeiro), desligou as mensagens privadas de bots (`bot_dms_disabled`) ou há um bloqueio entre ela e o bot (`blocked`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"no_shared_guild":{"value":{"error":"no_shared_guild","message":"o bot só manda mensagem privada para quem está num servidor com ele"}},"bot_dms_disabled":{"value":{"error":"bot_dms_disabled","message":"essa pessoa desligou as mensagens privadas de bots"}},"blocked":{"value":{"error":"blocked","message":"não dá para mandar mensagem para essa pessoa"}}}}}},"404":{"description":"O id não é de conta nenhuma.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"user_not_found","message":"usuário não encontrado"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/guilds/{guild_id}/members/{user_id}":{"get":{"operationId":"getBotGuildMember","tags":["Bots"],"summary":"Ler um membro","description":"Um membro, com os cargos dele e se ele tem o cargo escolhido para este bot ao adicioná-lo (a pergunta que o bot faz no cartão, como \"Quem pode chamar o Troia?\"). Leia na hora de decidir: um cargo dado ou tirado agora já vem na resposta. `@me` no lugar do id lê o próprio bot.","security":[{"botToken":[]}],"parameters":[{"name":"guild_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]}},{"name":"user_id","in":"path","required":true,"schema":{"type":"string","pattern":"^(\\d{1,19}|@me)$"}}],"responses":{"200":{"description":"O membro.","content":{"application/json":{"schema":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","minItems":1,"maxItems":1,"items":{"$ref":"#/components/schemas/BotMemberDetail"}}}}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"404":{"description":"O bot não está neste servidor (`guild_not_found`), ou a conta não é membro dele (`member_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"guild_not_found":{"value":{"error":"guild_not_found","message":"servidor não encontrado"}},"member_not_found":{"value":{"error":"member_not_found","message":"este usuário não é membro do servidor"}}}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}},"delete":{"operationId":"kickBotGuildMember","tags":["Bots"],"summary":"Expulsar um membro","description":"`@me` no lugar do id tira o próprio bot do servidor, sem exigir Expulsar membros — sair não é um poder de moderação. Expulsar outra conta exige Expulsar membros, e o dono do servidor nunca é expulso; como um bot nunca é dono, só a forma de outra pessoa tentando existe aqui (o 400 de quem tenta sair sendo dono é da API que uma pessoa usa, spec §7.2).","security":[{"botToken":[]}],"parameters":[{"name":"guild_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]}},{"name":"user_id","in":"path","required":true,"schema":{"type":"string","pattern":"^(\\d{1,19}|@me)$"}}],"responses":{"204":{"description":"Fora do servidor."},"401":{"$ref":"#/components/responses/BotInvalidToken"},"403":{"description":"O alvo é o dono do servidor (`owner_cannot_be_kicked`), ou falta Expulsar membros (`missing_permission`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"owner_cannot_be_kicked":{"value":{"error":"owner_cannot_be_kicked","message":"o dono do servidor não pode ser expulso"}},"missing_permission":{"value":{"error":"missing_permission","message":"esta ação exige a permissão KICK_MEMBERS","details":{"permission":"KICK_MEMBERS"}}}}}}},"404":{"description":"O bot não está neste servidor (`guild_not_found`), ou o alvo não é membro dele (`member_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"guild_not_found":{"value":{"error":"guild_not_found","message":"servidor não encontrado"}},"member_not_found":{"value":{"error":"member_not_found","message":"este usuário não é membro do servidor"}}}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/guilds/{guild_id}/bans/{user_id}":{"put":{"operationId":"banBotGuildMember","tags":["Bots"],"summary":"Banir alguém","description":"Exige Expulsar membros — não há uma permissão de banimento à parte. Banir o dono do servidor é `400` (`owner_cannot_be_banned`); expulsá-lo é `403` (`owner_cannot_be_kicked`) — a mesma proteção, duas respostas diferentes por enquanto (assimetria conhecida, spec §7.2).","security":[{"botToken":[]}],"parameters":[{"name":"guild_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]}},{"name":"user_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id de quem é banido.","examples":["1834021894356992001"]}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotBanRequest"},"example":{"reason":"spam"}}}},"responses":{"204":{"description":"Banido."},"400":{"description":"O alvo é o dono do servidor (`owner_cannot_be_banned`), ou é o próprio bot (`cannot_ban_self`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"owner_cannot_be_banned":{"value":{"error":"owner_cannot_be_banned","message":"o dono não pode ser banido"}},"cannot_ban_self":{"value":{"error":"cannot_ban_self","message":"não dá para banir a si mesmo"}}}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"403":{"description":"Falta Expulsar membros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_permission","message":"esta ação exige a permissão KICK_MEMBERS","details":{"permission":"KICK_MEMBERS"}}}}},"404":{"description":"O bot não está neste servidor (`guild_not_found`), ou o alvo não existe (`user_not_found` — inclui `@me`, que esta rota não aceita).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"guild_not_found":{"value":{"error":"guild_not_found","message":"servidor não encontrado"}},"user_not_found":{"value":{"error":"user_not_found","message":"usuário não encontrado"}}}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/guilds/{guild_id}/widget":{"put":{"operationId":"putBotWidget","tags":["Bots"],"summary":"Publicar o widget do bot","description":"O cartão que o bot mantém no pé da lista de membros desse servidor. O bot manda DADOS, nunca HTML: o app desenha o cartão com os componentes dele, então nada que o bot publica vira código na tela de quem olha. É um `PUT` porque o widget é um valor — mandar o mesmo cartão duas vezes deixa um cartão — e cada chamada substitui o cartão inteiro (não há remendo parcial). Todo mundo que enxerga o servidor VÊ o cartão; só quem está na sala de voz indicada em `voiceChannelId` pode apertar os botões, e um toque chega ao bot como o evento `widget.action` no gateway. Um cartão que ninguém renova expira em 15 minutos, e o de um bot que desconecta cai na hora. Não precisa de permissão nenhuma além de estar no servidor: o widget é o bot falando de si mesmo, não uma ação sobre o servidor.","security":[{"botToken":[]}],"parameters":[{"name":"guild_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotWidget"},"example":{"kind":"player","track":{"title":"Numb","subtitle":"3:07","requestedBy":"marina"},"playing":true,"queue":[{"title":"In the End","requestedBy":"rafa"}],"queueTotal":1,"actions":["skip","stop"],"voiceChannelId":"94963277674577920"}}}},"responses":{"200":{"description":"Publicado."},"400":{"description":"O cartão não tem a forma esperada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_body","message":"corpo inválido"}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"404":{"description":"O bot não está neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"guild_not_found","message":"servidor não encontrado"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}},"delete":{"operationId":"deleteBotWidget","tags":["Bots"],"summary":"Tirar o widget do bot","description":"Tira o cartão desse servidor, na hora, para todo mundo que estiver olhando. Um bot que desconecta não precisa chamar isto — o cartão cai sozinho.","security":[{"botToken":[]}],"parameters":[{"name":"guild_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]}}],"responses":{"204":{"description":"Tirado (ou já não havia cartão)."},"401":{"$ref":"#/components/responses/BotInvalidToken"},"404":{"description":"O bot não está neste servidor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"guild_not_found","message":"servidor não encontrado"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}},"/v1/channels/{channel_id}/voice":{"post":{"operationId":"joinBotVoice","tags":["Bots"],"summary":"Entrar numa sala de voz (WHIP)","description":"Envia a oferta SDP de um WebRTC só de envio (RFC 9725). O Gamerfy confere Ver canais, Conectar e Falar, e responde a resposta SDP com o endereço da sessão em `Location`. O áudio vai direto do bot ao servidor de mídia; por esta entrada o bot nunca recebe áudio da sala (para ouvir, veja `POST /v1/channels/{channel_id}/voice/listen`). Um bot fica em uma sala por servidor: entrar em outra sala DO MESMO servidor tira da primeira; em servidores diferentes, uma sala em cada. No máximo 6 entradas por minuto na mesma sala (429 `rate_limited` com `Retry-After`).","security":[{"botToken":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do canal.","examples":["1834021894356992001"]}}],"requestBody":{"required":true,"content":{"application/sdp":{"schema":{"type":"string"},"example":"v=0\r\n…"}}},"responses":{"201":{"description":"Na sala.","headers":{"Location":{"description":"A sessão: `DELETE` nesse endereço sai da sala.","schema":{"type":"string","examples":["/v1/voice/sessions/3f2b7f4e-2a4c-4b1d-9e55-0c2d7c1e9a10"]}},"Link":{"description":"Servidores ICE do servidor de mídia (RFC 9725 §4.6): `stun:`, `turn:` e `turns:`, com `username` e `credential` nos TURN; o TURN/TLS vem na porta que o Gamerfy publica (5349). Um cliente atrás de NAT restritivo ou sem UDP até o servidor de mídia não conecta com a primeira oferta — os servidores só chegam aqui: refaça a entrada (`DELETE` na sessão e um `POST` novo com uma oferta só de relay usando estes servidores). O SDK faz isso sozinho.","schema":{"type":"string"}},"ETag":{"description":"A sessão ICE.","schema":{"type":"string"}}},"content":{"application/sdp":{"schema":{"type":"string"},"example":"v=0\r\n…"}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"403":{"description":"Falta Ver canais, Conectar ou Falar.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_permission","message":"esta ação exige a permissão SPEAK","details":{"permission":"SPEAK"}}}}},"404":{"description":"Sala inexistente, que não é de voz, ou de servidor sem o bot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"channel_not_found","message":"sala de voz não encontrada"}}}},"413":{"description":"A oferta passa de 64 kB — junte os candidatos ICE, não mande um arquivo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"payload_too_large","message":"corpo da requisição maior que o limite aceito"}}}},"415":{"description":"O corpo não é `application/sdp`, ou está vazio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unsupported_media_type","message":"envie a oferta WHIP como application/sdp"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"502":{"description":"O servidor de mídia recusou a entrada, ou respondeu algo que não é uma resposta WHIP.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"refused":{"value":{"error":"voice_unavailable","message":"a voz recusou a entrada deste bot"}},"not_an_answer":{"value":{"error":"voice_unavailable","message":"a voz respondeu algo que não é uma resposta WHIP"}}}}}},"503":{"description":"O servidor de mídia não respondeu, ou parou de responder no meio (`voice_unavailable`); ou a API de bots não está ligada neste nó (`bots_unavailable`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"voice_unavailable":{"value":{"error":"voice_unavailable","message":"a voz está indisponível agora; tente de novo em instantes"}},"bots_unavailable":{"value":{"error":"bots_unavailable","message":"a API de bots não está disponível neste servidor"}}}}}}}}},"/v1/channels/{channel_id}/voice/listen":{"post":{"operationId":"listenBotVoice","tags":["Bots"],"summary":"Entrar numa sala de voz ouvindo","description":"Para bots com a permissão Ouvir a sala, marcada por um administrador. O Gamerfy confere Ver canais, Conectar, Falar e Ouvir a sala, e responde com o endereço e o token do servidor de mídia: o SDK (`voice.listen`) conecta com eles, publica o microfone do bot e recebe a voz de cada pessoa separada. O bot aparece na sala marcado como ouvindo, e quem entra é avisado. Mesmas regras da entrada WHIP: uma sala por servidor, no máximo 6 entradas por minuto na mesma sala (somando as duas entradas) e `DELETE` no endereço de `Location` para sair. O token é do SDK: ele não abre nada além desta sala, com estas permissões.","security":[{"botToken":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do canal.","examples":["1834021894356992001"]}}],"responses":{"201":{"description":"Pode conectar.","headers":{"Location":{"description":"A sessão: `DELETE` nesse endereço sai da sala.","schema":{"type":"string","examples":["/v1/voice/sessions/3f2b7f4e-2a4c-4b1d-9e55-0c2d7c1e9a10"]}}},"content":{"application/json":{"schema":{"type":"object","required":["session_id","url","token"],"properties":{"session_id":{"type":"string","description":"A sessão, a mesma do `Location`."},"url":{"type":"string","description":"O endereço do servidor de mídia."},"token":{"type":"string","description":"O token desta entrada, para o SDK."}}}}}},"401":{"$ref":"#/components/responses/BotInvalidToken"},"403":{"description":"Falta Ver canais, Conectar, Falar ou Ouvir a sala.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"missing_permission","message":"esta ação exige a permissão LISTEN_VOICE","details":{"permission":"LISTEN_VOICE"}}}}},"404":{"description":"Sala inexistente, que não é de voz, ou de servidor sem o bot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"channel_not_found","message":"sala de voz não encontrada"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"description":"A voz não está configurada neste nó (`voice_unavailable`); ou a API de bots não está ligada neste nó (`bots_unavailable`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"voice_unavailable":{"value":{"error":"voice_unavailable","message":"a voz está indisponível agora; tente de novo em instantes"}},"bots_unavailable":{"value":{"error":"bots_unavailable","message":"a API de bots não está disponível neste servidor"}}}}}}}}},"/v1/voice/sessions/{session_id}":{"delete":{"operationId":"leaveBotVoice","tags":["Bots"],"summary":"Sair da sala de voz","security":[{"botToken":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"}}],"responses":{"204":{"description":"Fora da sala."},"401":{"$ref":"#/components/responses/BotInvalidToken"},"404":{"description":"Sessão inexistente, encerrada ou de outro bot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"voice_session_not_found","message":"sessão de voz não encontrada"}}}},"429":{"$ref":"#/components/responses/BotRateLimited"},"503":{"$ref":"#/components/responses/BotsUnavailable"}}}}},"webhooks":{"stream.online":{"post":{"operationId":"onStreamOnlineEvent","tags":["Webhooks"],"summary":"stream.online","description":"Uma live pessoal foi admitida: o mesmo instante em que ela entra em `/v1/streams`. `data` é o item de lá, com `viewer_count: 0`.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamOnlineEvent"},"example":{"id":"1834400000000000101","type":"stream.online","created_at":"2026-09-09T21:30:00.482Z","data":{"id":"1834100000000000042","user_id":"1834021894356992001","user_login":"henrique","user_name":"Henrique","game_id":"pubg","title":"Ranked de terça","viewer_count":0,"started_at":"2026-09-09T21:30:00.123Z","type":"live","recording_id":"1834100000000000042"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"stream.offline":{"post":{"operationId":"onStreamOfflineEvent","tags":["Webhooks"],"summary":"stream.offline","description":"A live pessoal encerrou. O `id` é o mesmo que a stream tinha no ar.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamOfflineEvent"},"example":{"id":"1834400000000000102","type":"stream.offline","created_at":"2026-09-09T23:44:10.020Z","data":{"id":"1834100000000000042","user_id":"1834021894356992001","user_login":"henrique","user_name":"Henrique","type":"offline","started_at":"2026-09-09T21:30:00.123Z","ended_at":"2026-09-09T23:44:09.870Z"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"video.ready":{"post":{"operationId":"onVideoReadyEvent","tags":["Webhooks"],"summary":"video.ready","description":"O replay ficou pronto na CDN, segundos depois do fim da live. Só para replays **públicos**. `data` é o item de `/v1/videos`.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoReadyEvent"},"example":{"id":"1834400000000000103","type":"video.ready","created_at":"2026-09-09T23:44:15.301Z","data":{"id":"1834100000000000042","user_id":"1834021894356992001","user_login":"henrique","user_name":"Henrique","title":"Ranked de terça","created_at":"2026-09-08T21:30:00.123Z","published_at":"2026-09-08T23:44:09.870Z","duration":"2h14m9s","duration_seconds":8049,"url":"https://gamerfy.gg/videos/1834100000000000042","game_id":"pubg","type":"archive","viewable":"public","thumbnail_url":"https://vod.gamerfy.gg/bcdn_token=Xq3v9ZtL2kWm8RpA&token_path=%2Fvods%2F1834100000000000042%2F&expires=1789000000/vods/1834100000000000042/thumbnail.jpg","view_count":1840}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"clip.ready":{"post":{"operationId":"onClipReadyEvent","tags":["Webhooks"],"summary":"clip.ready","description":"Um clipe terminou de ser cortado. Só clipes **públicos**. `data` é o item de `/v1/clips`.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClipReadyEvent"},"example":{"id":"1834400000000000104","type":"clip.ready","created_at":"2026-09-09T22:05:31.774Z","data":{"id":"1834200000000000077","url":"https://gamerfy.gg/clips/1834200000000000077","embed_url":null,"broadcaster_id":"1834021894356992001","broadcaster_name":"Henrique","creator_id":"1834021894356992002","creator_name":"Fulano","video_id":"1834100000000000042","game_id":"pubg","title":"Headshot do último círculo","view_count":12400,"created_at":"2026-09-09T13:21:44.512Z","thumbnail_url":"https://vod.gamerfy.gg/bcdn_token=Xq3v9ZtL2kWm8RpA&token_path=%2Fclips%2F1834200000000000077%2F&expires=1789000000/clips/1834200000000000077/thumbnail.jpg","duration":30,"vod_offset":4415}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.update":{"post":{"operationId":"onChannelUpdateEvent","tags":["Webhooks"],"summary":"channel.update","description":"O streamer mudou o título ou o jogo da live. Chega de toda live pública, como `stream.online`.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelUpdateEvent"},"example":{"id":"1834400000000000105","type":"channel.update","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","title":"Ranked de terça","game_id":"pubg","game_name":"PUBG: Battlegrounds"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.follow":{"post":{"operationId":"onChannelFollowEvent","tags":["Webhooks"],"summary":"channel.follow","description":"Alguém seguiu o streamer. **Só das lives de quem autorizou o seu app** com `followers:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelFollowEvent"},"example":{"id":"1834400000000000106","type":"channel.follow","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","user_id":"1834021894356992077","user_login":"fulano","user_name":"Fulano","followed_at":"2026-09-29T21:47:03.120Z"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.subscribe":{"post":{"operationId":"onChannelSubscribeEvent","tags":["Webhooks"],"summary":"channel.subscribe","description":"O pagamento de uma assinatura foi confirmado (nova ou renovação). **Só das lives de quem autorizou o seu app** com `subscriptions:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelSubscribeEvent"},"example":{"id":"1834400000000000107","type":"channel.subscribe","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","user_id":"1834021894356992077","user_login":"fulano","user_name":"Fulano","ends_at":"2026-10-29T21:47:03.120Z","is_renewal":false}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.subscription.end":{"post":{"operationId":"onChannelSubscriptionEndEvent","tags":["Webhooks"],"summary":"channel.subscription.end","description":"Uma assinatura acabou sem renovação. **Só das lives de quem autorizou o seu app** com `subscriptions:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelSubscriptionEndEvent"},"example":{"id":"1834400000000000108","type":"channel.subscription.end","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","user_id":"1834021894356992077","user_login":"fulano","user_name":"Fulano"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.gift":{"post":{"operationId":"onChannelGiftEvent","tags":["Webhooks"],"summary":"channel.gift","description":"O pagamento de um presente foi confirmado. **Só das lives de quem autorizou o seu app** com `gifts:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelGiftEvent"},"example":{"id":"1834400000000000109","type":"channel.gift","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","gift_id":"1834400000000000077","user_id":"1834021894356992077","user_login":"fulano","user_name":"Fulano","tier":"crown","amount_cents":2500,"message":"Bora, Henrique!","message_state":"shown"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.gift.message_update":{"post":{"operationId":"onChannelGiftMessageUpdateEvent","tags":["Webhooks"],"summary":"channel.gift.message_update","description":"O streamer liberou ou recusou a mensagem de um presente que esperava por ele. **Só das lives de quem autorizou o seu app** com `gifts:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelGiftMessageUpdateEvent"},"example":{"id":"1834400000000000110","type":"channel.gift.message_update","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","gift_id":"1834400000000000077","message":"Bora, Henrique!","message_state":"shown"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.raid":{"post":{"operationId":"onChannelRaidEvent","tags":["Webhooks"],"summary":"channel.raid","description":"Um raid saiu: chega a quem mandou e a quem recebeu. **Só das lives de quem autorizou o seu app** com `raids:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelRaidEvent"},"example":{"id":"1834400000000000121","type":"channel.raid","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","from_broadcaster_user_id":"1834021894356992077","from_broadcaster_user_login":"bia","from_broadcaster_user_name":"Bia","to_broadcaster_user_id":"1834021894356992001","to_broadcaster_user_login":"henrique","to_broadcaster_user_name":"Henrique","viewers":31,"message":"Bora dar aquela força!"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.goal.begin":{"post":{"operationId":"onChannelGoalBeginEvent","tags":["Webhooks"],"summary":"channel.goal.begin","description":"Uma meta foi criada. **Só das lives de quem autorizou o seu app** com `goals:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelGoalBeginEvent"},"example":{"id":"1834400000000000111","type":"channel.goal.begin","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","goal_id":"1834400000000000088","type":"gifts","title":"Cadeira nova","current_amount":0,"target_amount":50000,"unit":"cents","show_on_live":true,"started_at":"2026-09-29T21:00:00.000Z","ends_at":null,"achieved_at":null,"ended_at":null,"end_reason":null}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.goal.progress":{"post":{"operationId":"onChannelGoalProgressEvent","tags":["Webhooks"],"summary":"channel.goal.progress","description":"A meta andou — no máximo um aviso por segundo por meta. **Só das lives de quem autorizou o seu app** com `goals:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelGoalProgressEvent"},"example":{"id":"1834400000000000112","type":"channel.goal.progress","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","goal_id":"1834400000000000088","type":"gifts","title":"Cadeira nova","current_amount":12500,"target_amount":50000,"unit":"cents","show_on_live":true,"started_at":"2026-09-29T21:00:00.000Z","ends_at":null,"achieved_at":null,"ended_at":null,"end_reason":null}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.goal.achieved":{"post":{"operationId":"onChannelGoalAchievedEvent","tags":["Webhooks"],"summary":"channel.goal.achieved","description":"A meta chegou ao alvo pela primeira vez. **Só das lives de quem autorizou o seu app** com `goals:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelGoalAchievedEvent"},"example":{"id":"1834400000000000113","type":"channel.goal.achieved","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","goal_id":"1834400000000000088","type":"gifts","title":"Cadeira nova","current_amount":50000,"target_amount":50000,"unit":"cents","show_on_live":true,"started_at":"2026-09-29T21:00:00.000Z","ends_at":null,"achieved_at":"2026-09-29T23:10:00.000Z","ended_at":null,"end_reason":null}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.goal.end":{"post":{"operationId":"onChannelGoalEndEvent","tags":["Webhooks"],"summary":"channel.goal.end","description":"A meta foi encerrada, trocada por outra ou venceu o prazo (o motivo vem em end_reason). **Só das lives de quem autorizou o seu app** com `goals:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelGoalEndEvent"},"example":{"id":"1834400000000000114","type":"channel.goal.end","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","goal_id":"1834400000000000088","type":"gifts","title":"Cadeira nova","current_amount":12500,"target_amount":50000,"unit":"cents","show_on_live":true,"started_at":"2026-09-29T21:00:00.000Z","ends_at":null,"achieved_at":null,"ended_at":"2026-09-29T23:40:00.000Z","end_reason":"ended"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.poll.begin":{"post":{"operationId":"onChannelPollBeginEvent","tags":["Webhooks"],"summary":"channel.poll.begin","description":"Uma enquete abriu. **Só das lives de quem autorizou o seu app** com `polls:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelPollBeginEvent"},"example":{"id":"1834400000000000115","type":"channel.poll.begin","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","poll_id":"1834400000000000099","title":"Qual mapa agora?","eligibility":"everyone","choices":[{"id":"a","title":"Mirage","votes":0},{"id":"b","title":"Inferno","votes":0}],"total_votes":0,"started_at":"2026-09-29T22:00:00.000Z","ends_at":"2026-09-29T22:03:00.000Z","ended_at":null,"close_reason":null}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.poll.progress":{"post":{"operationId":"onChannelPollProgressEvent","tags":["Webhooks"],"summary":"channel.poll.progress","description":"Votos novos — no máximo um aviso por segundo por enquete. **Só das lives de quem autorizou o seu app** com `polls:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelPollProgressEvent"},"example":{"id":"1834400000000000116","type":"channel.poll.progress","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","poll_id":"1834400000000000099","title":"Qual mapa agora?","eligibility":"everyone","choices":[{"id":"a","title":"Mirage","votes":41},{"id":"b","title":"Inferno","votes":26}],"total_votes":67,"started_at":"2026-09-29T22:00:00.000Z","ends_at":"2026-09-29T22:03:00.000Z","ended_at":null,"close_reason":null}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}},"channel.poll.end":{"post":{"operationId":"onChannelPollEndEvent","tags":["Webhooks"],"summary":"channel.poll.end","description":"A enquete fechou, com o resultado final (o motivo vem em close_reason). **Só das lives de quem autorizou o seu app** com `polls:read`: sem autorização, ou depois que o streamer tira o acesso em Conexões, o aviso não sai — nem as novas tentativas de um que já estava na fila.\n\nResponda **2xx em até 10 segundos**; qualquer outra coisa conta como falha e a entrega é repetida.","parameters":[{"name":"Gamerfy-Webhook-Id","in":"header","required":true,"description":"O `id` do envelope.","schema":{"type":"string"}},{"name":"Gamerfy-Webhook-Timestamp","in":"header","required":true,"description":"Segundos Unix do envio desta tentativa.","schema":{"type":"string"}},{"name":"Gamerfy-Signature","in":"header","required":true,"description":"`sha256=<hex>`: HMAC-SHA256 com o `secret` sobre `\"{id}.{timestamp}.{corpo bruto}\"`.","schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelPollEndEvent"},"example":{"id":"1834400000000000117","type":"channel.poll.end","created_at":"2026-09-29T21:47:03.120Z","data":{"broadcaster_user_id":"1834021894356992001","broadcaster_user_login":"henrique","broadcaster_user_name":"Henrique","poll_id":"1834400000000000099","title":"Qual mapa agora?","eligibility":"everyone","choices":[{"id":"a","title":"Mirage","votes":41},{"id":"b","title":"Inferno","votes":26}],"total_votes":67,"started_at":"2026-09-29T22:00:00.000Z","ends_at":"2026-09-29T22:03:00.000Z","ended_at":"2026-09-29T22:03:00.000Z","close_reason":"expired"}}}}},"responses":{"2XX":{"description":"Recebido. Não é preciso corpo."}}}}},"components":{"securitySchemes":{"appToken":{"type":"http","scheme":"bearer","bearerFormat":"gfa_…","description":"O `access_token` de `POST /oauth2/token`."},"botToken":{"type":"apiKey","in":"header","name":"Authorization","description":"`Bot gfb_…` — o token que a aba Desenvolvedor mostra ao ligar o bot."},"userToken":{"type":"oauth2","description":"A chave de um streamer que autorizou o app (`gfp_…`, 4 horas), por código com PKCE (S256).","flows":{"authorizationCode":{"authorizationUrl":"https://gamerfy.gg/autorizar","tokenUrl":"https://api.gamerfy.gg/oauth2/token","refreshUrl":"https://api.gamerfy.gg/oauth2/token","scopes":{"live:read":"Ver título, jogo e quantas pessoas assistem","chat:read":"Ler o chat da live","chat:write":"Escrever no chat da live, com o nome do app","chat:moderate":"Apagar mensagens do chat da live","followers:read":"Saber quando alguém segue","subscriptions:read":"Saber quando alguém assina","gifts:read":"Saber quando alguém manda presente, com a mensagem","goals:read":"Ver as metas","polls:read":"Ver as enquetes e os votos","polls:manage":"Abrir e encerrar enquetes","alerts:read":"Ver a fila dos alertas"}}}}},"schemas":{"Error":{"type":"object","description":"Todo erro da API. `error` é o código estável (compare com ele, nunca com a mensagem); `message` é para humanos, em pt-BR, e pode mudar.","required":["error"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Código estável do erro.","examples":["invalid_token"]},"message":{"type":"string","description":"Explicação em pt-BR; pode mudar.","examples":["token de app ausente, inválido ou expirado"]},"details":{"description":"Em `invalid_body`/`invalid_query`: um item por campo recusado. Em `rate_limited` da API de bots: quanto esperar. Em `missing_permission`: qual permissão faltou.","oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/ValidationIssue"}},{"$ref":"#/components/schemas/RetryAfter"},{"type":"object","required":["permission"],"additionalProperties":false,"properties":{"permission":{"type":"string","examples":["SEND_MESSAGES"]}}}]},"method":{"type":"string","description":"Só em `not_found`: o método pedido."},"path":{"type":"string","description":"Só em `not_found`: o caminho pedido."}}},"ValidationIssue":{"type":"object","required":["path","message"],"additionalProperties":false,"properties":{"path":{"type":"string","description":"O campo recusado.","examples":["first"]},"message":{"type":"string","description":"Por que foi recusado.","examples":["Too big: expected number to be <=100"]}}},"Pagination":{"type":"object","description":"Vem com `cursor` enquanto houver mais páginas e vazio (`{}`) quando não há. Uma página cheia sempre traz cursor, mesmo quando por acaso é a última.","additionalProperties":false,"properties":{"cursor":{"type":"string","description":"Opaco: repita a chamada com `after=<cursor>` e os mesmos filtros.","examples":["eyJzdGFydGVkX2F0Ijoi"]}}},"TokenRequest":{"type":"object","required":["client_id","grant_type"],"additionalProperties":false,"properties":{"client_id":{"type":"string","minLength":1,"maxLength":64,"description":"O client id do app (30 caracteres `[a-z0-9]`).","examples":["k3v9x2m7q1w8e4r6t0y5u3i9o2p7a1"]},"client_secret":{"type":"string","minLength":1,"maxLength":128,"description":"O segredo do app, `gfs_` + 40 caracteres. Só no servidor de quem integra. Obrigatório em `client_credentials`; opcional nos outros dois, em que o PKCE é a prova.","examples":["gfs_Q2w3E4r5T6y7U8i9O0p1A2s3D4f5G6h7J8k9L0z1"]},"grant_type":{"type":"string","enum":["client_credentials","authorization_code","refresh_token"],"description":"`client_credentials` para os dados públicos; `authorization_code` troca o código da autorização de um streamer; `refresh_token` renova as chaves dele."},"code":{"type":"string","maxLength":128,"description":"Em `authorization_code`: o `code` que voltou no endereço de volta. Vale 60 segundos e uma vez só."},"redirect_uri":{"type":"string","maxLength":500,"description":"Em `authorization_code`: o mesmo endereço de volta da autorização."},"code_verifier":{"type":"string","maxLength":128,"description":"Em `authorization_code`: o segredo do PKCE (S256) de onde saiu o `code_challenge`."},"refresh_token":{"type":"string","maxLength":128,"description":"Em `refresh_token`: a `gfr_` atual. Usar uma que já foi trocada tira a autorização inteira."}}},"UserToken":{"type":"object","required":["access_token","refresh_token","token_type","expires_in","scope"],"additionalProperties":false,"description":"As chaves de um app autorizado por um streamer (`authorization_code` e `refresh_token`).","properties":{"access_token":{"type":"string","pattern":"^gfp_[A-Za-z0-9]{40}$","description":"Lê a live do streamer por 4 horas.","examples":["gfp_4kQ9mB2xT7pL0vN8rY3wZ6cH1jF5dS9aK2eU4oI7"]},"refresh_token":{"type":"string","pattern":"^gfr_[A-Za-z0-9]{40}$","description":"Renova as duas por 60 dias; troque uma vez só.","examples":["gfr_7yT2pL9mK4xB1vN6rQ3wZ8cH5jF0dS2aK7eU1oI4"]},"token_type":{"type":"string","enum":["bearer"]},"expires_in":{"type":"integer","description":"Segundos até a `access_token` expirar (4 horas).","examples":[14400]},"scope":{"type":"string","description":"As permissões autorizadas, separadas por espaço.","examples":["chat:read gifts:read"]}}},"UserTokenValidation":{"type":"object","required":["client_id","user_id","login","scopes","expires_in"],"additionalProperties":false,"properties":{"client_id":{"type":"string","examples":["k3v9x2m7q1w8e4r6t0y5u3i9o2p7a1"]},"user_id":{"type":"string","description":"O streamer que autorizou.","examples":["1834021894356992001"]},"login":{"type":"string","examples":["bia_ferreira"]},"scopes":{"type":"array","items":{"type":"string"},"examples":[["chat:read","gifts:read"]]},"expires_in":{"type":"integer","minimum":0,"description":"Segundos que ainda faltam.","examples":[14000]}}},"Token":{"type":"object","required":["access_token","expires_in","token_type"],"additionalProperties":false,"properties":{"access_token":{"type":"string","pattern":"^gfa_[A-Za-z0-9]{40}$","description":"Token opaco; guarde e reutilize até expirar.","examples":["gfa_4kQ9mB2xT7pL0vN8rY3wZ6cH1jF5dS9aK2eU4oI7"]},"expires_in":{"type":"integer","description":"Segundos até expirar (60 dias).","examples":[5184000]},"token_type":{"type":"string","enum":["bearer"]}}},"TokenValidation":{"type":"object","required":["client_id","app_name","expires_in"],"additionalProperties":false,"properties":{"client_id":{"type":"string","examples":["k3v9x2m7q1w8e4r6t0y5u3i9o2p7a1"]},"app_name":{"type":"string","examples":["pubg.report"]},"expires_in":{"type":"integer","minimum":0,"description":"Segundos que ainda faltam.","examples":[5180000]}}},"UserLink":{"type":"object","description":"Uma conta de jogo que o usuário vinculou e deixou visível no perfil.","required":["game_id","account_id","account_name"],"additionalProperties":false,"properties":{"game_id":{"type":"string","description":"Slug do jogo (o `id` de `/v1/games`).","examples":["pubg"]},"account_id":{"type":"string","description":"No PUBG, o `accountId` da API oficial da KRAFTON.","examples":["account.c0e530e9b7244b358def282782f893de"]},"account_name":{"type":"string","description":"O nome do jogador na época do vínculo.","examples":["HenriqueTJ"]}}},"User":{"type":"object","required":["id","login","display_name","profile_image_url","created_at","links"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do usuário.","examples":["1834021894356992001"]},"login":{"type":"string","description":"O username do Gamerfy, sem `@`.","examples":["henrique"]},"display_name":{"type":"string","examples":["Henrique"]},"profile_image_url":{"type":["string","null"],"format":"uri","description":"`null` para quem não tem foto.","examples":["https://gamerfy.gg/api/users/1834021894356992001/avatar?v=3f9a1c2b"]},"created_at":{"type":"string","format":"date-time","description":"Quando a conta foi criada.","examples":["2026-09-08T21:30:00.123Z"]},"links":{"type":"array","items":{"$ref":"#/components/schemas/UserLink"},"description":"Contas de jogo vinculadas e visíveis. É o campo que dispensa adivinhar o jogador pelo nome do canal. `[]` quando não há."}}},"Stream":{"type":"object","description":"Uma live pessoal no ar agora.","required":["id","user_id","user_login","user_name","game_id","title","viewer_count","started_at","type","recording_id"],"additionalProperties":false,"properties":{"id":{"type":"string","description":"O mesmo id que a gravação terá como replay em `/v1/videos`. Sem gravação ligada, é `{user_id}-{epoch em segundos de started_at}`.","examples":["1834100000000000042"]},"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id de quem está ao vivo.","examples":["1834021894356992001"]},"user_login":{"type":"string","examples":["henrique"]},"user_name":{"type":"string","examples":["Henrique"]},"game_id":{"type":["string","null"],"description":"Slug do jogo; `null` quando a live não diz.","examples":["pubg"]},"title":{"type":"string","examples":["Ranked de terça"]},"viewer_count":{"type":"integer","minimum":0,"description":"Quantas pessoas estão assistindo agora: contas conectadas E visitantes sem sessão (quem abre a live pelo site, sem entrar). Cada uma conta uma vez, e some ~2 min depois do último sinal do player.","examples":[37]},"started_at":{"type":"string","format":"date-time","description":"Quando a live foi admitida.","examples":["2026-09-08T21:30:00.123Z"]},"type":{"type":"string","enum":["live"]},"recording_id":{"type":["string","null"],"description":"O id do replay que está sendo gravado; `null` quando a gravação não está ligada.","examples":["1834100000000000042"]}}},"Video":{"type":"object","description":"Um replay de live pessoal, pronto, público e dentro da retenção de 14 dias.","required":["id","user_id","user_login","user_name","title","created_at","published_at","duration","duration_seconds","url","game_id","type","viewable","thumbnail_url","view_count"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do replay.","examples":["1834021894356992001"]},"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do streamer.","examples":["1834021894356992001"]},"user_login":{"type":"string","examples":["henrique"]},"user_name":{"type":"string","examples":["Henrique"]},"title":{"type":"string","examples":["Ranked de terça"]},"created_at":{"type":"string","format":"date-time","description":"O relógio de parede do **primeiro quadro gravado** (`EXT-X-PROGRAM-DATE-TIME`), em UTC com milissegundos. É a âncora do offset: `offset = event_time_utc − created_at`, sem recuo.","examples":["2026-09-08T21:30:00.123Z"]},"published_at":{"type":["string","null"],"format":"date-time","description":"Quando a live terminou.","examples":["2026-09-08T23:44:09.870Z"]},"duration":{"type":"string","description":"No formato da Twitch: `1h2m3s`, `12m5s`, `45s`.","examples":["2h14m9s"]},"duration_seconds":{"type":"integer","minimum":0,"examples":[8049]},"url":{"type":"string","format":"uri","description":"A página pública do replay; aceita `?t=1h2m3s`.","examples":["https://gamerfy.gg/videos/1834100000000000042"]},"game_id":{"type":["string","null"],"examples":["pubg"]},"type":{"type":"string","enum":["archive"]},"viewable":{"type":"string","enum":["public"],"description":"Replays privados não saem na API."},"thumbnail_url":{"type":["string","null"],"format":"uri","description":"JPEG de 640 px do primeiro segundo. URL **assinada por 6 horas**: não guarde, peça de novo. `null` quando o quadro não pôde ser tirado.","examples":["https://vod.gamerfy.gg/bcdn_token=Xq3v9ZtL2kWm8RpA&token_path=%2Fvods%2F1834100000000000042%2F&expires=1789000000/vods/1834100000000000042/thumbnail.jpg"]},"view_count":{"type":"integer","minimum":0,"description":"Pessoas distintas por dia, somadas dia a dia: cada conta, ou cada visitante, conta no máximo uma vez por dia neste replay. Não é o total de reproduções da Twitch. Atualizado a cada 10 minutos.","examples":[1840]}}},"Clip":{"type":"object","description":"Um clipe de 5 a 60 segundos, pronto e público.","required":["id","url","embed_url","broadcaster_id","broadcaster_name","creator_id","creator_name","video_id","game_id","title","view_count","created_at","thumbnail_url","duration","vod_offset"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do clipe.","examples":["1834021894356992001"]},"url":{"type":"string","format":"uri","description":"A página pública do clipe.","examples":["https://gamerfy.gg/clips/1834200000000000077"]},"embed_url":{"type":"null","description":"Sempre `null` (sem embed por iframe ainda)."},"broadcaster_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_name":{"type":"string","examples":["Henrique"]},"creator_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Quem cortou o clipe; pode ser o próprio streamer.","examples":["1834021894356992001"]},"creator_name":{"type":"string","examples":["Fulano"]},"video_id":{"type":["string","null"],"description":"O replay de origem; `null` quando ele já expirou ou foi apagado.","examples":["1834100000000000042"]},"game_id":{"type":["string","null"],"description":"O jogo do clipe; `null` quando o clipe não diz.","examples":["pubg"]},"title":{"type":"string","examples":["Headshot do último círculo"]},"view_count":{"type":"integer","minimum":0,"description":"Pessoas distintas por dia, somadas dia a dia: cada conta, ou cada visitante, conta no máximo uma vez por dia neste clipe. Não é o total de reproduções da Twitch. Atualizado a cada 10 minutos.","examples":[12400]},"created_at":{"type":"string","format":"date-time","description":"Quando o clipe foi pedido (não quando ficou pronto).","examples":["2026-09-08T21:30:00.123Z"]},"thumbnail_url":{"type":["string","null"],"format":"uri","description":"JPEG assinado por 6 horas.","examples":["https://vod.gamerfy.gg/bcdn_token=Xq3v9ZtL2kWm8RpA&token_path=%2Fclips%2F1834200000000000077%2F&expires=1789000000/clips/1834200000000000077/thumbnail.jpg"]},"duration":{"type":"number","minimum":5,"maximum":60,"description":"Em **segundos**, como na Twitch.","examples":[30]},"vod_offset":{"type":"number","minimum":0,"description":"Em que segundo do replay o clipe começa: `https://gamerfy.gg/videos/{video_id}?t={vod_offset}s`.","examples":[4415]}}},"Game":{"type":"object","required":["id","name","twitch_category_id","steam_app_id"],"additionalProperties":false,"properties":{"id":{"type":"string","description":"O slug; é o `game_id` de todas as outras rotas.","examples":["pubg"]},"name":{"type":"string","examples":["PUBG: Battlegrounds"]},"twitch_category_id":{"type":["string","null"],"description":"Para cruzar com a Twitch.","examples":["493057"]},"steam_app_id":{"type":["integer","null"],"description":"Para cruzar com a Steam.","examples":[578080]}}},"WebhookEvent":{"type":"string","enum":["stream.online","stream.offline","video.ready","clip.ready","channel.update","channel.follow","channel.subscribe","channel.subscription.end","channel.gift","channel.gift.message_update","channel.raid","channel.goal.begin","channel.goal.progress","channel.goal.achieved","channel.goal.end","channel.poll.begin","channel.poll.progress","channel.poll.end"],"description":"A lista fechada de eventos. `stream.online`, `stream.offline`, `video.ready`, `clip.ready` e `channel.update` chegam de toda live pública. **Os outros só chegam das lives de quem autorizou o seu app** com a permissão do tipo (`followers:read`, `subscriptions:read`, `gifts:read`, `goals:read`, `polls:read`): sem autorização, o aviso não sai."},"WebhookRequest":{"type":"object","required":["url","events"],"additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"`https://` obrigatório, host público, sem usuário e senha e sem fragmento.","examples":["https://hooks.pubg.report/gamerfy"]},"events":{"type":"array","minItems":1,"maxItems":32,"uniqueItems":true,"items":{"$ref":"#/components/schemas/WebhookEvent"}}}},"WebhookCreated":{"type":"object","required":["id","url","events","secret","created_at"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do webhook.","examples":["1834021894356992001"]},"url":{"type":"string","format":"uri","examples":["https://hooks.pubg.report/gamerfy"]},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}},"secret":{"type":"string","description":"A chave do HMAC. **Aparece só nesta resposta.**","examples":["u3Zk8sQ1pV6nW2yR4tE7oA9cL5mB0xH3jG1fD8kS2qN"]},"created_at":{"type":"string","format":"date-time","description":"Quando foi registrado.","examples":["2026-09-08T21:30:00.123Z"]}}},"Webhook":{"type":"object","required":["id","url","events","failures","disabled_at","created_at"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do webhook.","examples":["1834021894356992001"]},"url":{"type":"string","format":"uri","examples":["https://hooks.pubg.report/gamerfy"]},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}},"failures":{"type":"integer","minimum":0,"description":"Falhas seguidas; a primeira resposta 2xx zera, a 20.ª desliga.","examples":[0]},"disabled_at":{"type":["string","null"],"format":"date-time","description":"Quando foi desligado por falhas; `null` enquanto ativo."},"created_at":{"type":"string","format":"date-time","description":"Quando foi registrado.","examples":["2026-09-08T21:30:00.123Z"]}}},"OfflineStream":{"type":"object","description":"A live que acabou de encerrar: o mesmo `id` que ela tinha no ar.","required":["id","user_id","user_login","user_name","type","started_at","ended_at"],"additionalProperties":false,"properties":{"id":{"type":"string","examples":["1834100000000000042"]},"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do streamer.","examples":["1834021894356992001"]},"user_login":{"type":"string","examples":["henrique"]},"user_name":{"type":"string","examples":["Henrique"]},"type":{"type":"string","enum":["offline"]},"started_at":{"type":"string","format":"date-time","description":"Quando a live começou.","examples":["2026-09-08T21:30:00.123Z"]},"ended_at":{"type":"string","format":"date-time","description":"Quando a live terminou.","examples":["2026-09-08T21:30:00.123Z"]}}},"ChannelUpdate":{"type":"object","description":"O streamer mudou o título ou o jogo da live.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","title","game_id","game_name"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"title":{"type":"string","maxLength":140,"examples":["Ranked de terça"]},"game_id":{"type":["string","null"],"description":"O slug do jogo; `null` sem jogo.","examples":["pubg"]},"game_name":{"type":["string","null"],"examples":["PUBG: Battlegrounds"]}}},"ChannelFollow":{"type":"object","description":"Alguém começou a seguir o streamer. Um mesmo seguidor que sai e volta no mesmo dia não gera outro.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","user_id","user_login","user_name","followed_at"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Quem fez.","examples":["1834021894356992001"]},"user_login":{"type":"string","examples":["fulano"]},"user_name":{"type":"string","examples":["Fulano"]},"followed_at":{"type":"string","format":"date-time","description":"Quando seguiu.","examples":["2026-09-08T21:30:00.123Z"]}}},"ChannelSubscribe":{"type":"object","description":"O pagamento de uma assinatura foi confirmado: nova ou renovação.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","user_id","user_login","user_name","ends_at","is_renewal"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Quem fez.","examples":["1834021894356992001"]},"user_login":{"type":"string","examples":["fulano"]},"user_name":{"type":"string","examples":["Fulano"]},"ends_at":{"type":"string","format":"date-time","description":"Até quando a assinatura vale.","examples":["2026-09-08T21:30:00.123Z"]},"is_renewal":{"type":"boolean","description":"`true` numa renovação.","examples":[false]}}},"ChannelSubscriptionEnd":{"type":"object","description":"Uma assinatura acabou sem renovação.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","user_id","user_login","user_name"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Quem fez.","examples":["1834021894356992001"]},"user_login":{"type":"string","examples":["fulano"]},"user_name":{"type":"string","examples":["Fulano"]}}},"ChannelGift":{"type":"object","description":"O pagamento de um presente foi confirmado. A mensagem só vem quando passou no filtro (`message_state: shown`); nos outros estados ela é `null`.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","gift_id","user_id","user_login","user_name","tier","amount_cents","message","message_state"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"gift_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do presente.","examples":["1834021894356992001"]},"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Quem fez.","examples":["1834021894356992001"]},"user_login":{"type":"string","examples":["fulano"]},"user_name":{"type":"string","examples":["Fulano"]},"tier":{"type":"string","enum":["confetti","crown","rocket"],"examples":["crown"]},"amount_cents":{"type":"integer","minimum":1,"description":"Em **centavos**, nunca fração.","examples":[2500]},"message":{"type":["string","null"],"minLength":1,"maxLength":140,"examples":["Bora, Henrique!"]},"message_state":{"type":"string","enum":["none","shown","filtered","pending"],"description":"`none`: sem mensagem; `shown`: passou no filtro; `filtered`: o filtro barrou; `pending`: espera o streamer liberar.","examples":["shown"]}}},"ChannelRaid":{"type":"object","description":"Um raid saiu: quem mandou levou a audiência dele para a live de quem recebeu. Chega aos dois canais; `broadcaster_*` diz de quem é o evento.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","from_broadcaster_user_id","from_broadcaster_user_login","from_broadcaster_user_name","to_broadcaster_user_id","to_broadcaster_user_login","to_broadcaster_user_name","viewers","message"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"from_broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Quem mandou o raid.","examples":["1834021894356992001"]},"from_broadcaster_user_login":{"type":"string","examples":["bia"]},"from_broadcaster_user_name":{"type":"string","examples":["Bia"]},"to_broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Quem recebeu o raid.","examples":["1834021894356992001"]},"to_broadcaster_user_login":{"type":"string","examples":["henrique"]},"to_broadcaster_user_name":{"type":"string","examples":["Henrique"]},"viewers":{"type":"integer","minimum":0,"description":"Quantas contas o raid levou.","examples":[31]},"message":{"type":["string","null"],"minLength":1,"maxLength":140,"description":"A mensagem de quem mandou, quando escreveu uma.","examples":["Bora dar aquela força!"]}}},"ChannelGiftMessageUpdate":{"type":"object","description":"O streamer liberou (`shown`) ou recusou (`rejected`) a mensagem de um presente que esperava por ele.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","gift_id","message","message_state"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"gift_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do presente.","examples":["1834021894356992001"]},"message":{"type":["string","null"],"minLength":1,"maxLength":140,"description":"Só com `shown`.","examples":["Bora, Henrique!"]},"message_state":{"type":"string","enum":["shown","rejected"],"examples":["shown"]}}},"Goal":{"type":"object","description":"Uma meta da live.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","goal_id","type","title","current_amount","target_amount","unit","show_on_live","started_at","ends_at","achieved_at","ended_at","end_reason"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"goal_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da meta.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["gifts","followers","subscribers"],"description":"O que a meta conta.","examples":["gifts"]},"title":{"type":"string","minLength":1,"maxLength":40,"examples":["Cadeira nova"]},"current_amount":{"type":"integer","description":"Quanto já foi: centavos numa meta de presentes, pessoas nas outras.","examples":[12500]},"target_amount":{"type":"integer","minimum":1,"examples":[50000]},"unit":{"type":"string","enum":["cents","count"],"examples":["cents"]},"show_on_live":{"type":"boolean","description":"Se a barra aparece na live para quem assiste.","examples":[true]},"started_at":{"type":"string","format":"date-time","description":"Quando a meta foi criada.","examples":["2026-09-08T21:30:00.123Z"]},"ends_at":{"type":["string","null"],"format":"date-time","description":"O prazo; `null` sem prazo."},"achieved_at":{"type":["string","null"],"format":"date-time","description":"A primeira vez que chegou ao alvo."},"ended_at":{"type":["string","null"],"format":"date-time","description":"Quando foi encerrada."},"end_reason":{"type":["string","null"],"enum":["ended","replaced","expired",null],"description":"Só em `channel.goal.end`: `ended` (o streamer encerrou), `replaced` (uma meta nova tomou o lugar), `expired` (venceu o prazo)."}}},"PollChoice":{"type":"object","required":["id","title","votes"],"additionalProperties":false,"properties":{"id":{"type":"string","minLength":1,"maxLength":8,"examples":["a"]},"title":{"type":"string","minLength":1,"maxLength":25,"examples":["Mirage"]},"votes":{"type":"integer","minimum":0,"examples":[41]}}},"Poll":{"type":"object","description":"Uma enquete da live.","required":["broadcaster_user_id","broadcaster_user_login","broadcaster_user_name","poll_id","title","eligibility","choices","total_votes","started_at","ends_at","ended_at","close_reason"],"additionalProperties":false,"properties":{"broadcaster_user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"De quem é a live (o `user_id` das outras rotas).","examples":["1834021894356992001"]},"broadcaster_user_login":{"type":"string","examples":["henrique"]},"broadcaster_user_name":{"type":"string","examples":["Henrique"]},"poll_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da enquete.","examples":["1834021894356992001"]},"title":{"type":"string","minLength":1,"maxLength":60,"examples":["Qual mapa agora?"]},"eligibility":{"type":"string","enum":["everyone","followers","subscribers"],"description":"Quem pode votar.","examples":["everyone"]},"choices":{"type":"array","minItems":2,"maxItems":5,"items":{"$ref":"#/components/schemas/PollChoice"}},"total_votes":{"type":"integer","minimum":0,"examples":[67]},"started_at":{"type":"string","format":"date-time","description":"Quando abriu.","examples":["2026-09-08T21:30:00.123Z"]},"ends_at":{"type":["string","null"],"format":"date-time","description":"Quando fecha sozinha; `null` numa enquete sem prazo, que só o streamer (ou o fim da live) encerra."},"ended_at":{"type":["string","null"],"format":"date-time","description":"Quando fechou."},"close_reason":{"type":["string","null"],"enum":["expired","ended","live_ended",null],"description":"Só em `channel.poll.end`: `expired` (o tempo acabou), `ended` (o streamer fechou), `live_ended` (a live acabou)."}}},"BotUser":{"type":"object","required":["id","username","display_name","avatar_url","bot"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da conta.","examples":["1834021894356992001"]},"username":{"type":"string","examples":["henrique"]},"display_name":{"type":"string","examples":["Henrique"]},"avatar_url":{"type":["string","null"],"format":"uri","examples":["https://backend.gamerfy.gg/api/users/1834021894356992001/avatar?v=3f9a1c2b"]},"bot":{"type":"boolean","description":"`true` quando a conta é de um bot.","examples":[false]}}},"BotSelf":{"type":"object","required":["id","username","display_name","avatar_url","bot","app_id"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da conta do bot.","examples":["1834021894356992001"]},"username":{"type":"string","examples":["dj_gamer"]},"display_name":{"type":"string","examples":["DJ Gamer"]},"avatar_url":{"type":["string","null"],"format":"uri"},"bot":{"const":true},"app_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do app dono do bot.","examples":["1834021894356992001"]}}},"BotGuildSummary":{"type":"object","required":["id","name","icon_url","permissions"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]},"name":{"type":"string","examples":["Clã dos Amigos"]},"icon_url":{"type":["string","null"],"format":"uri"},"permissions":{"type":"string","pattern":"^\\d{1,19}$","description":"As permissões do bot no servidor, em decimal.","examples":["8963"]}}},"BotChannel":{"type":"object","required":["id","guild_id","type","name","position","permissions"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da sala ou do canal.","examples":["1834021894356992001"]},"guild_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["text","voice","live"]},"name":{"type":"string","examples":["Jogando"]},"position":{"type":"integer","examples":[0]},"permissions":{"type":"string","pattern":"^\\d{1,19}$","description":"As permissões do bot nesta sala, em decimal.","examples":["8963"]}}},"BotVoiceState":{"type":"object","required":["user_id","channel_id"],"additionalProperties":false,"properties":{"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id de quem está na sala.","examples":["1834021894356992001"]},"channel_id":{"type":"string","pattern":"^\\d{1,19}$","description":"A sala de voz em que está.","examples":["1834021894356992001"]}}},"BotGuild":{"type":"object","required":["id","name","icon_url","permissions","channels","voice_states"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]},"name":{"type":"string","examples":["Clã dos Amigos"]},"icon_url":{"type":["string","null"],"format":"uri"},"permissions":{"type":"string","pattern":"^\\d{1,19}$","description":"As permissões do bot no servidor, em decimal.","examples":["8963"]},"channels":{"type":"array","items":{"$ref":"#/components/schemas/BotChannel"},"description":"Só as salas e canais em que o bot tem Ver canais."},"voice_states":{"type":"array","items":{"$ref":"#/components/schemas/BotVoiceState"},"description":"Quem está em qual sala de voz, só das salas que o bot enxerga — conferido contra o LiveKit antes de responder."}}},"BotMember":{"type":"object","required":["user","nickname","joined_at"],"additionalProperties":false,"properties":{"user":{"$ref":"#/components/schemas/BotUser"},"nickname":{"type":["string","null"]},"joined_at":{"type":"string","format":"date-time","description":"Quando entrou no servidor.","examples":["2026-09-08T21:30:00.123Z"]}}},"BotMemberDetail":{"type":"object","required":["user","nickname","joined_at","role_ids","holds_chosen_role"],"additionalProperties":false,"properties":{"user":{"$ref":"#/components/schemas/BotUser"},"nickname":{"type":["string","null"]},"joined_at":{"type":"string","format":"date-time","description":"Quando entrou no servidor.","examples":["2026-09-08T21:30:00.123Z"]},"role_ids":{"type":"array","items":{"type":"string","pattern":"^\\d{1,19}$","description":"Id de um cargo do membro.","examples":["1834021894356992001"]},"description":"Os cargos do membro, sem o @everyone (que todos têm)."},"holds_chosen_role":{"type":["boolean","null"],"description":"Se o membro tem o cargo escolhido para este bot quando ele foi adicionado. `null` quando o bot não pede cargo; `false` quando pede e o cargo foi apagado. Escolher o @everyone vale para todos."}}},"BotMention":{"type":"object","required":["user_id","username"],"additionalProperties":false,"properties":{"user_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id de quem foi mencionado.","examples":["1834021894356992001"]},"username":{"type":"string","examples":["dj_gamer"]}}},"BotAttachment":{"type":"object","required":["id","name","content_type","size","url"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do anexo.","examples":["1834021894356992001"]},"name":{"type":"string","examples":["print.png"]},"content_type":{"type":"string","examples":["image/png"]},"size":{"type":"integer","examples":[48213]},"url":{"type":"string","format":"uri"}}},"BotMessage":{"type":"object","required":["id","guild_id","channel_id","author","content","mentions","attachments","created_at","edited_at"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da mensagem.","examples":["1834021894356992001"]},"guild_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do servidor.","examples":["1834021894356992001"]},"channel_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do canal.","examples":["1834021894356992001"]},"author":{"$ref":"#/components/schemas/BotUser"},"content":{"type":["string","null"],"description":"`null` quando o bot não tem Ler mensagens e não foi mencionado.","examples":["@dj_gamer tocar lofi"]},"mentions":{"type":"array","items":{"$ref":"#/components/schemas/BotMention"}},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/BotAttachment"},"description":"Vazia quando `content` é `null`."},"created_at":{"type":"string","format":"date-time","description":"Quando foi escrita.","examples":["2026-09-08T21:30:00.123Z"]},"edited_at":{"type":["string","null"],"format":"date-time"}}},"BotMessageRequest":{"type":"object","required":["content"],"additionalProperties":false,"properties":{"content":{"type":"string","minLength":1,"maxLength":2000,"examples":["Tocando lofi"]}}},"BotDirectMessage":{"type":"object","required":["id","channel_id","recipient_id","author","content","created_at"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da mensagem.","examples":["1834021894356992001"]},"channel_id":{"type":"string","pattern":"^\\d{1,19}$","description":"A conversa entre o bot e a pessoa: a mesma em toda mensagem seguinte para ela.","examples":["1834021894356992001"]},"recipient_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Para quem o bot escreveu.","examples":["1834021894356992001"]},"author":{"$ref":"#/components/schemas/BotUser"},"content":{"type":"string","examples":["Seu link: https://exemplo.com/mcp/abc"]},"created_at":{"type":"string","format":"date-time","description":"Quando foi escrita.","examples":["2026-09-08T21:30:00.123Z"]}}},"BotBanRequest":{"type":"object","additionalProperties":false,"properties":{"reason":{"type":"string","minLength":1,"maxLength":200,"examples":["spam"]}}},"BotWidgetTrack":{"type":"object","required":["title"],"additionalProperties":false,"properties":{"title":{"type":"string","minLength":1,"maxLength":120,"examples":["Numb"]},"subtitle":{"type":["string","null"],"minLength":1,"maxLength":120,"examples":["Linkin Park"]},"artUrl":{"type":["string","null"],"format":"uri","maxLength":500,"description":"Capa, só `https`. O navegador de QUEM OLHA busca essa imagem, então o servidor apontado aqui fica sabendo o ip de todo mundo que está vendo a lista de membros: aponte para um host seu, ou deixe de fora.","examples":["https://exemplo.com/capa.jpg"]},"requestedBy":{"type":["string","null"],"minLength":1,"maxLength":120,"examples":["marina"]}}},"BotWidget":{"type":"object","required":["kind"],"additionalProperties":false,"description":"O cartão do bot. Hoje existe um tipo só, `player`.","properties":{"kind":{"type":"string","enum":["player"]},"track":{"oneOf":[{"$ref":"#/components/schemas/BotWidgetTrack"},{"type":"null"}],"description":"`null` segura o espaço sem tocar nada."},"playing":{"type":"boolean","default":false},"progress":{"type":["number","null"],"minimum":0,"maximum":1,"description":"De 0 a 1. `null` quando não há fim para ser fração de."},"queue":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/BotWidgetTrack"},"description":"O cartão mostra no máximo 3; o resto vira \"e mais N\"."},"queueTotal":{"type":"integer","minimum":0,"maximum":10000,"description":"Quantas estão MESMO esperando — só o bot sabe contar."},"actions":{"type":"array","maxItems":5,"items":{"type":"string","enum":["pause","resume","skip","previous","stop"]},"description":"Quais botões desenhar. Um toque em botão que não está aqui é recusado."},"voiceChannelId":{"type":["string","null"],"pattern":"^\\d{1,19}$","description":"A sala de voz onde o bot toca: só quem está nela pode apertar os botões. Sem ela, o cartão é só de leitura."}}},"RetryAfter":{"type":"object","required":["retry_after_ms"],"additionalProperties":false,"properties":{"retry_after_ms":{"type":"integer","minimum":1,"examples":[3200]}}},"UsersResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}},"StreamsResponse":{"type":"object","required":["data","pagination"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Stream"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"VideosResponse":{"type":"object","required":["data","pagination"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Video"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"ClipsResponse":{"type":"object","required":["data","pagination"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Clip"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"GamesResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}}},"WebhookCreatedResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookCreated"}}},"description":"Uma lista de um item, no molde da Twitch."},"WebhooksResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}},"PollsResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Poll"}}},"description":"As enquetes, no molde da Twitch: `{ data: [ … ] }`, a aberta primeiro."},"PollCreateRequest":{"type":"object","required":["broadcaster_id","title","choices"],"additionalProperties":false,"properties":{"broadcaster_id":{"type":"string","pattern":"^\\d{1,19}$","description":"O streamer que autorizou o app: o dono da chave.","examples":["1834021894356992001"]},"title":{"type":"string","minLength":1,"maxLength":60,"examples":["Qual mapa agora?"]},"choices":{"type":"array","minItems":2,"maxItems":5,"items":{"type":"object","required":["title"],"additionalProperties":false,"properties":{"title":{"type":"string","minLength":1,"maxLength":25,"examples":["Mirage"]}}}},"duration":{"type":["integer","null"],"minimum":15,"maximum":1800,"description":"Segundos. Sem o campo (ou `null`), a enquete não tem prazo: fica aberta até um `PATCH` ou o fim da live.","examples":[180]},"eligibility":{"type":"string","enum":["everyone","followers","subscribers"],"description":"Quem vota; `everyone` por padrão.","examples":["everyone"]}}},"ChatMessageRequest":{"type":"object","required":["broadcaster_id","message"],"additionalProperties":false,"properties":{"broadcaster_id":{"type":"string","pattern":"^\\d{1,19}$","description":"O streamer que autorizou o app: o dono da chave.","examples":["1834021894356992001"]},"message":{"type":"string","minLength":1,"maxLength":500,"description":"A linha, em texto simples. Não pode começar com `/`.","examples":["Lembrete: sem spoiler no chat!"]}}},"ChatMessageSent":{"type":"object","required":["message_id","is_sent"],"properties":{"message_id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da linha: o mesmo que `channel.chat.message` traz, e o que um `DELETE` nomeia.","examples":["1834021894356992001"]},"is_sent":{"type":"boolean","description":"Sempre `true` numa resposta 200: uma linha recusada responde com o erro, nunca com `false`.","examples":[true]}}},"ChatMessageSentResponse":{"type":"object","required":["data"],"properties":{"data":{"type":"array","minItems":1,"maxItems":1,"items":{"$ref":"#/components/schemas/ChatMessageSent"}}}},"PollEndRequest":{"type":"object","required":["broadcaster_id","id","status"],"additionalProperties":false,"properties":{"broadcaster_id":{"type":"string","pattern":"^\\d{1,19}$","description":"O streamer que autorizou o app: o dono da chave.","examples":["1834021894356992001"]},"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id da enquete.","examples":["1834021894356992001"]},"status":{"type":"string","enum":["TERMINATED"],"description":"Encerrar agora."}}},"StreamOnlineEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["stream.online"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Stream"}}},"StreamOfflineEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["stream.offline"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/OfflineStream"}}},"VideoReadyEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["video.ready"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Video"}}},"ClipReadyEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["clip.ready"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Clip"}}},"ChannelUpdateEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.update"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/ChannelUpdate"}}},"ChannelFollowEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.follow"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/ChannelFollow"}}},"ChannelSubscribeEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.subscribe"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/ChannelSubscribe"}}},"ChannelSubscriptionEndEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.subscription.end"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/ChannelSubscriptionEnd"}}},"ChannelGiftEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.gift"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/ChannelGift"}}},"ChannelGiftMessageUpdateEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.gift.message_update"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/ChannelGiftMessageUpdate"}}},"ChannelRaidEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.raid"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/ChannelRaid"}}},"RaidStartRequest":{"type":"object","required":["from_broadcaster_id","to_broadcaster_id"],"additionalProperties":false,"properties":{"from_broadcaster_id":{"type":"string","pattern":"^\\d{1,19}$","description":"O streamer que autorizou o app: o dono da chave, que manda o raid.","examples":["1834021894356992001"]},"to_broadcaster_id":{"type":"string","pattern":"^\\d{1,19}$","description":"A live que recebe o raid.","examples":["1834021894356992001"]}}},"RaidStarted":{"type":"object","required":["id","created_at","ends_at","waiting_answer"],"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do raid.","examples":["1834021894356992001"]},"created_at":{"type":"string","format":"date-time","description":"Quando a contagem começou.","examples":["2026-09-08T21:30:00.123Z"]},"ends_at":{"type":"string","format":"date-time","description":"Quando o raid sai sozinho (60 s depois do começo).","examples":["2026-09-08T21:30:00.123Z"]},"waiting_answer":{"type":"boolean","description":"`true` quando quem recebe pergunta a cada raid e ainda não respondeu: o raid só sai depois do sim.","examples":[false]}}},"RaidStartedResponse":{"type":"object","required":["data"],"properties":{"data":{"type":"array","minItems":1,"maxItems":1,"items":{"$ref":"#/components/schemas/RaidStarted"}}}},"ChannelGoalBeginEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.goal.begin"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Goal"}}},"ChannelGoalProgressEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.goal.progress"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Goal"}}},"ChannelGoalAchievedEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.goal.achieved"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Goal"}}},"ChannelGoalEndEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.goal.end"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Goal"}}},"ChannelPollBeginEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.poll.begin"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Poll"}}},"ChannelPollProgressEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.poll.progress"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Poll"}}},"ChannelPollEndEvent":{"type":"object","required":["id","type","created_at","data"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^\\d{1,19}$","description":"Id do **evento**: o mesmo em todos os webhooks e em todas as tentativas. Use para descartar repetições.","examples":["1834021894356992001"]},"type":{"type":"string","enum":["channel.poll.end"]},"created_at":{"type":"string","format":"date-time","description":"Quando o evento aconteceu; é a ordem real entre eventos.","examples":["2026-09-08T21:30:00.123Z"]},"data":{"$ref":"#/components/schemas/Poll"}}},"BotSelfResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BotSelf"}}}},"BotGuildsResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BotGuildSummary"}}}},"BotGuildResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BotGuild"}}}},"BotChannelsResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BotChannel"}}}},"BotMembersResponse":{"type":"object","required":["data","pagination"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BotMember"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"BotMessageResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BotMessage"}}}},"BotDirectMessageResponse":{"type":"object","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","minItems":1,"maxItems":1,"items":{"$ref":"#/components/schemas/BotDirectMessage"}}}}},"responses":{"InvalidQuery":{"description":"Parâmetro recusado; `details` diz qual.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_query","message":"parâmetros da consulta inválidos","details":[{"path":"first","message":"Too big: expected number to be <=100"}]}}}},"InvalidPageQuery":{"description":"`invalid_query` (parâmetro recusado; `details` diz qual) ou `invalid_cursor` (`after` corrompido).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_query":{"value":{"error":"invalid_query","message":"parâmetros da consulta inválidos","details":[{"path":"first","message":"Too big: expected number to be <=100"}]}},"invalid_cursor":{"value":{"error":"invalid_cursor","message":"o cursor `after` não é um cursor emitido por esta API"}}}}}},"InvalidToken":{"description":"Bearer ausente, com formato errado, desconhecido ou expirado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_token","message":"token de app ausente, inválido ou expirado"}}}},"RateLimited":{"description":"Mais de 800 pedidos no minuto: espere `Ratelimit-Reset` segundos.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"rate_limited","message":"limite de pedidos do app atingido; tente de novo quando a janela virar"}}}},"BotInvalidToken":{"description":"Token de bot ausente, com formato errado, revogado, ou expirado por 90 dias sem uso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_token":{"value":{"error":"invalid_token","message":"token de bot ausente, inválido ou revogado"}},"bot_token_expired":{"value":{"error":"bot_token_expired","message":"o token deste bot expirou por inatividade; gere um novo na aba Desenvolvedor"}}}}}},"BotsUnavailable":{"description":"A API de bots não está ligada neste nó. É do servidor, não do pedido: repita — em produção todo nó liga os bots.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"bots_unavailable","message":"a API de bots não está disponível neste servidor"}}}},"BotRateLimited":{"description":"Mais de 800 pedidos no minuto do app; em `POST /v1/channels/{channel_id}/messages`, também mais de 5 mensagens em 5 segundos no mesmo canal; em `POST /v1/users/{user_id}/messages`, também mais de 20 mensagens privadas por minuto, somando todas as pessoas; em `POST /v1/channels/{channel_id}/voice` e `POST /v1/channels/{channel_id}/voice/listen`, também mais de 6 entradas por minuto na mesma sala, somando as duas. Sempre `rate_limited` com `details.retry_after_ms`, e o cabeçalho `Retry-After` além dos `Ratelimit-*` — a diferença para o 429 da API de apps, que não traz nenhum dos dois.","headers":{"Ratelimit-Limit":{"description":"Pedidos por minuto do app (800).","schema":{"type":"integer","examples":[800]}},"Ratelimit-Remaining":{"description":"Quantos ainda cabem nesta janela.","schema":{"type":"integer","examples":[799]}},"Ratelimit-Reset":{"description":"Segundos até a janela virar.","schema":{"type":"integer","examples":[42]}},"Retry-After":{"description":"Segundos até poder tentar de novo (arredondado para cima).","schema":{"type":"integer","examples":[4]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"rate_limited","message":"limite de pedidos do app atingido; tente de novo quando a janela virar","details":{"retry_after_ms":3200}}}}}}}}