Gateway
O gateway é um WebSocket por onde o Zynko avisa o que acontece: mensagens novas, gente entrando, canais mudando. Um bot fica conectado nele o tempo todo e usa as rotas HTTP para agir.
wss://zynko.org/api/gatewayFrames
Tudo o que passa pelo gateway é texto JSON, um objeto por frame (o que o bot manda pode ter no máximo 64 KB):
json
{ "op": 0, "t": "MESSAGE_CREATE", "d": { "...": "..." } }| campo | descrição |
|---|---|
op | O opcode (abaixo). |
t | Nome do evento. Só em op 0. |
d | Os dados. Pode faltar em alguns opcodes. |
Não há número de sequência (s).
Opcodes
| op | nome | direção | o que é |
|---|---|---|---|
0 | DISPATCH | servidor → bot | Um evento. t diz qual. |
1 | HEARTBEAT | bot → servidor | "Ainda estou aqui." d é ignorado. |
2 | IDENTIFY | bot → servidor | Autentica a conexão. |
4 | VOICE_STATE | bot → servidor | Entra, sai ou muda o estado numa sala de voz. |
10 | HELLO | servidor → bot | Primeira mensagem, com o intervalo de heartbeat. |
11 | HEARTBEAT_ACK | servidor → bot | Resposta a cada heartbeat. |
Conectando
Abra o WebSocket. O servidor manda HELLO:
json{ "op": 10, "d": { "heartbeat_interval": 30000 } }Mande IDENTIFY em até 15 segundos:
json{ "op": 2, "d": { "token": "bot_3f9a...e1", "intents": 9 } }campo descrição tokenO token do bot, puro (sem Botna frente).intentsOpcional. Os tipos de evento que o bot quer, somados (veja Intents). Sem esse campo, chegam todos. O servidor responde com o evento READY (
op0,t: "READY"), com tudo o que a conta precisa saber (veja READY). A partir daí, chegam os eventos.Mande HEARTBEAT (
{ "op": 1 }) a cadaheartbeat_intervalmilissegundos. O servidor responde{ "op": 11 }.
Se o servidor passar 60 segundos (duas vezes o intervalo) sem receber nada da conexão, ele a fecha. Antes do IDENTIFY, um HEARTBEAT também é respondido; qualquer outro opcode fecha a conexão.
Intents
Os intents dizem que eventos a conexão quer receber. Um bot que só responde a comandos não precisa receber cada presença e cada "digitando..." de todos os servidores em que está.
| intent | bit | decimal | eventos |
|---|---|---|---|
GUILDS | 1 << 0 | 1 | GUILD_UPDATE, GUILD_ROLE_CREATE, GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, GUILD_EMOJIS_UPDATE, CHANNEL_UPDATE, CHANNEL_DELETE |
GUILD_MEMBERS | 1 << 1 | 2 | GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE |
PRESENCES | 1 << 2 | 4 | PRESENCE_UPDATE |
MESSAGES | 1 << 3 | 8 | MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_ACK (em servidores e DMs) |
REACTIONS | 1 << 4 | 16 | MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE |
TYPING | 1 << 5 | 32 | TYPING_START |
VOICE_STATES | 1 << 6 | 64 | VOICE_STATE_UPDATE |
Todos juntos: 127. Sem o campo intents, a conexão recebe todos (é o que os apps fazem); com "intents": 0, recebe só os que sempre chegam.
Sempre chegam, com qualquer intent: READY, GUILD_CREATE, GUILD_DELETE, CHANNEL_CREATE (uma DM nova é um canal novo), USER_UPDATE, INTERACTION_CREATE, INTERACTION_RESPONSE, RELATIONSHIP_ADD e RELATIONSHIP_REMOVE.
Que intents usar
Quase todo bot quer GUILDS (1): sem ele, a lista de canais e cargos que veio no READY fica desatualizada. Some MESSAGES (8) para ler mensagens: GUILDS | MESSAGES = 9.
Os intents só filtram o que chega. Eles não dão acesso a nada: o bot só recebe eventos dos canais que pode ver.
READY
O primeiro evento depois do IDENTIFY traz o estado inteiro da conta:
| campo | tipo | descrição |
|---|---|---|
user | PublicUser | A própria conta (o bot). |
guilds | GuildFull[] | Os servidores, com canais visíveis, membros, cargos, emojis e quem está online. |
dm_channels | Channel[] | As DMs, com recipients, das mais recentes para as mais antigas. |
read_states | ReadState[] | Até onde a conta leu cada canal. |
dm_unread | objeto | { "<channel_id>": n }: quantas mensagens não lidas há em cada DM. |
presences | string[] | Ids das contas relacionadas (servidores em comum, DMs, amigos) que estão online. |
relationships | Relationship[] | Amigos, pedidos e bloqueios. |
voice_states | VoiceState[] | Quem está nas salas de voz que a conta vê, e em chamadas nas DMs dela. |
session_id | string | Id desta conexão. Aparece em VoiceState.session_id. |
Reconectando
Não existe RESUME. Se a conexão cair, abra outra, mande IDENTIFY de novo e use o READY novo como o estado atual: ele traz tudo, então nada se perde além dos eventos do intervalo (busque mensagens que faltaram com GET /channels/{id}/messages?after=...).
Uma conta pode ter várias conexões abertas ao mesmo tempo; cada uma recebe os eventos que pediu. A conta aparece online enquanto houver pelo menos uma.
Se a conexão não der conta de ler os eventos (mais de 2048 na fila), o servidor a fecha com 4000; reconecte.
Códigos de fechamento
| código | motivo | o que fazer |
|---|---|---|
4000 | Sessão encerrada: o token foi trocado ou o bot apagado, ou a conexão ficou para trás nos eventos. | Reconectar. Se o IDENTIFY falhar com 4004, o token não vale mais. |
4003 | Não autenticado: IDENTIFY não chegou em 15 s, frame inválido, ou outro opcode antes do IDENTIFY. | Corrigir e reconectar. |
4004 | Falha de autenticação: token inválido. | Não reconectar com o mesmo token. |
1011 | Erro interno ao montar o READY. | Reconectar depois de alguns segundos. |
Quando o servidor fica 60 s sem ouvir a conexão, ele simplesmente fecha o socket.
Estado de voz (op 4)
Depois de pegar um token em POST /channels/{id}/voice, avise pelo gateway que a conta está na sala:
json
{
"op": 4,
"d": { "channel_id": "1234567890123456789", "self_mute": false, "self_deaf": false, "self_stream": false, "self_video": false }
}| campo | tipo | descrição |
|---|---|---|
channel_id | id? | A sala de voz (ou DM). null (ou ausente) = sair da voz. |
self_mute | boolean | Microfone desligado. Padrão false. |
self_deaf | boolean | Som desligado. Padrão false. |
self_stream | boolean | Compartilhando a tela. Padrão false. |
self_video | boolean | Câmera ligada. Padrão false. |
Mande de novo sempre que algo mudar (o estado inteiro, não só o que mudou). Quem vê a sala recebe VOICE_STATE_UPDATE. Uma conta fica em uma sala por vez: entrar em outra tira da anterior. Se não puder entrar (sem Entrar em salas, canal errado), o pedido é ignorado em silêncio.
O lugar na voz pertence à conexão do gateway que mandou o op 4. Se ela cair, a conta continua na sala por 15 segundos; se uma nova conexão não reclamar o lugar nesse tempo, a conta sai.
Exemplo
Um bot mínimo em JavaScript, sem bibliotecas (Node.js 22 ou Bun, que têm WebSocket global): conecta, se identifica, mantém o heartbeat e mostra as mensagens novas.
js
// gateway.js — ZYNKO_TOKEN=bot_... node gateway.js
const TOKEN = process.env.ZYNKO_TOKEN;
const GUILDS = 1 << 0;
const MESSAGES = 1 << 3;
function connect() {
const ws = new WebSocket('wss://zynko.org/api/gateway');
let heartbeat;
let me;
ws.addEventListener('message', (ev) => {
const { op, t, d } = JSON.parse(ev.data);
if (op === 10) {
// HELLO: identifique-se e comece o heartbeat.
ws.send(JSON.stringify({ op: 2, d: { token: TOKEN, intents: GUILDS | MESSAGES } }));
heartbeat = setInterval(() => ws.send(JSON.stringify({ op: 1 })), d.heartbeat_interval);
return;
}
if (op !== 0) return; // 11 = HEARTBEAT_ACK
if (t === 'READY') {
me = d.user;
console.log(`Conectado como ${me.username}, em ${d.guilds.length} servidores`);
} else if (t === 'MESSAGE_CREATE') {
if (d.author.id === me.id) return; // as mensagens do próprio bot também chegam
console.log(`#${d.channel_id} ${d.author.display_name}: ${d.content}`);
}
});
ws.addEventListener('close', (ev) => {
clearInterval(heartbeat);
if (ev.code === 4004) {
console.error('Token inválido.');
return;
}
console.log(`Conexão fechada (${ev.code}); reconectando em 5 s`);
setTimeout(connect, 5000);
});
}
connect();Para responder, chame a API com o mesmo token; veja Seu primeiro bot.