Respawn Tech Dev

Realtime

Publique e receba mensagens em tempo real por WebSocket ou SSE, com tokens emitidos no seu servidor, presença, histórico por canal, webhook de canais e reconexão sem perder mensagens.

Canais de publicação e assinatura por WebSocket (ou SSE, só leitura) para chat, notificações e painéis ao vivo. O seu servidor emite um token com as permissões de cada cliente e o navegador conecta direto no `connect_url` devolvido junto, sem expor a sua chave de API. Cada canal guarda um histórico recente, tem presença (quem está online) e avisa o seu servidor por webhook; a reconexão recupera o que foi perdido com `rewind`.

Base path
/v1/tools/realtime
Escopos
tools:realtime:publishtools:realtime:subscribetools:realtime:managetools:manage

Como funciona

O Realtime leva mensagens do seu servidor, ou de outros clientes, para os navegadores conectados, sem você manter servidores de WebSocket.

  • Canal: um nome livre de até 128 caracteres, com letras, números e : _ - . (chat:lobby, pedido:123). Quem assina o canal recebe o que for publicado nele. Não há nada para criar antes.
  • Token: credencial de curta duração que o seu servidor emite com a sua chave de API e entrega ao cliente. Ele diz quem é o cliente (client_id, opcional) e em quais canais pode assinar, publicar e usar a presença (capabilities).
  • connect_url: o endereço WebSocket para onde o cliente conecta, devolvido na mesma resposta do token e já com o ?token=.
  • Mensagem: data JSON publicada num canal, com id, timestamp (milissegundos Unix) e o client_id de quem publicou.
  • Presença: a lista de quem está conectado e "presente" num canal, com dados livres de cada membro (um apelido, um status).

O fluxo é sempre o mesmo:

O navegador pede um token ao seu servidor, que já sabe quem é o usuário.

O seu servidor emite o token na API com as capabilities daquele usuário e devolve ao navegador a resposta inteira: token, expires_at e connect_url.

O navegador conecta em connect_url, assina canais e publica.

Se a conexão cair, o navegador pede um token novo e reconecta, recuperando as mensagens perdidas com rewind.

Use sempre o connect_url da resposta

Nunca grave o endereço do WebSocket no código ou na configuração. O connect_url pode mudar entre ambientes (teste e produção) e quando a plataforma redistribui as conexões; um token só vale no endereço que veio com ele.

A sua chave de API (X-API-Token) fica só no servidor. O navegador usa apenas o token de conexão, que expira e só enxerga os canais que você liberou.

OperaçãoRota
Emitir token de conexãoPOST/tokens
Publicar mensagem pelo servidorPOST/channels/{channel}/messages
Consultar histórico do canalGET/channels/{channel}/history
Listar a presença do canalGET/channels/{channel}/presence
Listar canais ativosGET/channels
Consultar uso e conexõesGET/stats
Consultar o webhook de canaisGET/webhook
Configurar o webhook de canaisPUT/webhook
Trocar o secret do webhookPOST/webhook/rotate-secret
Remover o webhook de canaisDELETE/webhook

Token e capabilities

Emitir token de conexão

POST/v1/tools/realtime/tokens
Custo
Grátis
Dry-run
Não
Autenticação
qualquer um detools:realtime:managetools:manage
  • capabilities (obrigatório): objeto de padrão de canal para a lista de operações permitidas, até 100 padrões. O padrão é um nome exato (alerts), um prefixo terminado em * (chat:*) ou só * (todos os canais). As operações são subscribe, publish, presence (entrar na presença; ler a presença exige subscribe) ou * (todas). Sem capability para o canal, a operação falha com realtime.capability_denied.
  • client_id (opcional, até 128 caracteres): identifica o cliente nas mensagens que ele publica e na presença. Use o id do seu usuário; não aceite esse valor vindo do navegador.
  • ttl_seconds (opcional): validade do token, de 1 minuto até 24 horas, padrão 1 hora. O token só é checado ao conectar: uma conexão aberta continua valendo depois que ele expira, mas a próxima reconexão exige um token novo.

A resposta traz token, expires_at e connect_url (que já inclui ?token=). Quando a plataforma precisa ligar um nó antes de aceitar conexões, a resposta também traz retry_after_ms: o token é válido, mas espere esse tempo antes de conectar.

curl -sS -X POST 'https://api.respawntech.dev/v1/tools/realtime/tokens' \
  -H "X-API-Token: $RESPAWN_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "client_id": "user-42", "capabilities": { "chat:*": ["subscribe", "publish", "presence"], "alerts": ["subscribe"] }, "ttl_seconds": 3600 }'
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', {
  method: 'POST',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ "client_id": "user-42", "capabilities": { "chat:*": ["subscribe", "publish", "presence"], "alerts": ["subscribe"] }, "ttl_seconds": 3600 }),
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.post(
    "https://api.respawntech.dev/v1/tools/realtime/tokens",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    json={ "client_id": "user-42", "capabilities": { "chat:*": ["subscribe", "publish", "presence"], "alerts": ["subscribe"] }, "ttl_seconds": 3600 },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/tokens');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => <<<'JSON'
{ "client_id": "user-42", "capabilities": { "chat:*": ["subscribe", "publish", "presence"], "alerts": ["subscribe"] }, "ttl_seconds": 3600 }
JSON,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.respawntech.dev/v1/tools/realtime/tokens", strings.NewReader(`{ "client_id": "user-42", "capabilities": { "chat:*": ["subscribe", "publish", "presence"], "alerts": ["subscribe"] }, "ttl_seconds": 3600 }`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 201 — conecte direto em connect_url, que já traz o ?token=; se vier retry_after_ms, espere esse tempo antes de conectar

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJyZWFsdGltZSIsImNpZCI6InVzZXItNDIifQ.q3Zk1xYpL0",
    "expires_at": "2026-10-06T13:00:00Z",
    "connect_url": "wss://rt.respawntech.dev/v1/connect?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJyZWFsdGltZSIsImNpZCI6InVzZXItNDIifQ.q3Zk1xYpL0"
  }
}

Dê a cada cliente só o que ele precisa: um chat com {"chat:lobby": ["subscribe", "publish", "presence"]}, um painel somente leitura com {"metricas:*": ["subscribe"]}. O token é assinado com HMAC (HS256) e só vale para o Realtime.

Token do inspetor

O dashboard usa a rota POST /v1/tools/realtime/inspector-token para abrir o inspetor ao vivo (assinatura de todos os canais, sem publicar, 15 minutos). Ela só aceita a sessão do dashboard; com uma chave de API responde realtime.session_required.

Conexão pelo navegador

No seu servidor, exponha uma rota que emite o token e repassa a resposta. O exemplo abaixo usa RESPAWN_API_TOKEN e uma rota /realtime-token; troque o client_id pelo usuário autenticado.

import { createServer } from 'node:http';

createServer(async (req, res) => {
  if (req.url !== '/realtime-token') return res.writeHead(404).end();

  const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', {
    method: 'POST',
    headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: 'user-42', // o usuário autenticado
      capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] },
    }),
  });
  const { data } = await api.json();

  // token, expires_at, connect_url e retry_after_ms seguem juntos para o navegador
  res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));
}).listen(3000);
import os
import requests
from flask import Flask, jsonify

app = Flask(__name__)

@app.post("/realtime-token")
def realtime_token():
    api = requests.post(
        "https://api.respawntech.dev/v1/tools/realtime/tokens",
        headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"]},
        json={
            "client_id": "user-42",  # o usuário autenticado
            "capabilities": {"chat:*": ["subscribe", "publish", "presence"]},
        },
        timeout=30,
    )
    # token, expires_at, connect_url e retry_after_ms seguem juntos para o navegador
    return jsonify(api.json()["data"]), api.status_code

No navegador, peça o token ao seu servidor e conecte no connect_url que veio nele. Este HTML funciona colado em um arquivo e servido pelo mesmo servidor da rota /realtime-token (em teste, use uma chave do ambiente de teste); ele já reconecta sozinho. Se preferir não escrever o protocolo à mão, use o SDK JavaScript.

<!doctype html>
<meta charset="utf-8" />
<title>Realtime</title>
<ul id="log"></ul>
<input id="text" placeholder="Mensagem" />
<button id="send">Enviar</button>

<script>
  const CHANNEL = 'chat:lobby';
  const REWIND = 50; // mensagens recuperadas a cada (re)conexão
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const seen = new Set(); // ids já exibidos: o rewind pode repetir mensagens
  let ws = null;
  let attempt = 0;

  function show(text) {
    const li = document.createElement('li');
    li.textContent = text;
    document.getElementById('log').append(li);
  }

  async function connect() {
    // Um token novo a cada conexão: cobre expiração e o close 4001
    const res = await fetch('/realtime-token', { method: 'POST' });
    if (!res.ok) return retry();
    const { connect_url, retry_after_ms } = await res.json();
    if (retry_after_ms) await sleep(retry_after_ms);

    ws = new WebSocket(connect_url); // o connect_url já leva o ?token=

    ws.onmessage = (event) => {
      const frame = JSON.parse(event.data);
      if (frame.action === 'connected') {
        attempt = 0;
        ws.send(JSON.stringify({ action: 'subscribe', channel: CHANNEL, rewind: REWIND }));
      } else if (frame.action === 'message') {
        if (seen.has(frame.id)) return;
        seen.add(frame.id);
        show(`${frame.client_id ?? '?'}: ${JSON.stringify(frame.data)}`);
      } else if (frame.action === 'error') {
        console.warn('realtime error', frame.code, frame.message);
      }
    };

    ws.onclose = (event) => {
      // 4003: Realtime desativado para a conta, não reconecte.
      if (event.code === 4003) return;
      // 1012: reinício do nó, reconecte já. 1013 e 4029: backoff.
      // 4001: token inválido, o próximo connect() já emite outro.
      retry(event.code === 1012 ? 0 : undefined);
    };
  }

  async function retry(delay) {
    attempt += 1;
    // backoff exponencial com jitter, teto de 30 s
    const wait = delay ?? Math.random() * Math.min(30000, 500 * 2 ** attempt);
    await sleep(wait);
    connect();
  }

  document.getElementById('send').onclick = () => {
    const input = document.getElementById('text');
    if (ws?.readyState !== WebSocket.OPEN || !input.value) return;
    ws.send(JSON.stringify({ action: 'publish', channel: CHANNEL, data: { text: input.value } }));
    input.value = '';
  };

  connect();
</script>

Protocolo

As mensagens da conexão são frames de texto JSON, cada um com um campo action. Depois de conectar, espere o frame connected antes de assinar. O nome do canal segue a regra ^[A-Za-z0-9:_\-.]{1,128}$.

Do cliente para o servidor

actionCamposO que faz
subscribechannel, rewind?Assina o canal (exige subscribe no token). Com rewind (número de mensagens, até 100), reenvia as últimas mensagens do canal antes das novas. Responde attached.
unsubscribechannelDeixa de receber o canal. Responde detached.
publishchannel, name?, data, id?Publica data (JSON) no canal (exige publish). name classifica a mensagem. Se enviar id, o servidor responde ack com ele.
presenceop, channel, data?, id?Operações de presença: op é enter, update, leave ou get.

Do servidor para o cliente

actionCamposQuando
connectedconnection_idA conexão foi aceita; já pode assinar.
attachedchannelA assinatura do canal está ativa (depois do rewind, se pedido).
detachedchannelA assinatura foi encerrada.
messagechannel, id, name?, data, timestamp, client_id?Uma mensagem do canal, inclusive as que você mesmo publicou. timestamp é um inteiro em milissegundos Unix.
ackid, channel?A publicação (ou operação de presença) com esse id foi aceita.
presenceop, channel, member ou membersEvento de presença (enter, update, leave) ou resposta ao get (sync).
errorcode, message, channel?, id?Uma operação falhou. Os code estão em Erros. A conexão continua aberta.

Exemplo de troca:

← { "action": "connected", "connection_id": "c_8f3a1d" }
→ { "action": "subscribe", "channel": "chat:lobby", "rewind": 20 }
← { "action": "attached", "channel": "chat:lobby" }
→ { "action": "publish", "channel": "chat:lobby", "id": "m-1", "data": { "text": "Olá!" } }
← { "action": "message", "channel": "chat:lobby", "id": "1790338000000-0", "data": { "text": "Olá!" }, "timestamp": 1790338000000, "client_id": "user-42" }
← { "action": "ack", "id": "m-1" }

O id do message é gerado pelo servidor e é o que você usa para descartar repetições. O id do publish é só seu, para casar com o ack.

Close codes

CódigoSignificadoO que fazer
1012O nó está reiniciando (deploy ou manutenção).Reconecte já, com um pequeno jitter.
1013O nó está cheio.Reconecte com backoff exponencial.
4001Token inválido ou expirado (realtime.token_invalid).Emita um token novo e reconecte; repetir o mesmo token não adianta.
4003Realtime desativado para a conta (realtime.disabled).Não reconecte.
4029A conta atingiu o limite de conexões (realtime.connection_limit).Reconecte com backoff exponencial.
Qualquer outroQueda de rede ou erro inesperado.Reconecte com backoff exponencial.

SSE: leitura sem WebSocket

Para clientes que não conseguem abrir um WebSocket, o mesmo token abre um fluxo text/event-stream só de leitura, no mesmo servidor do connect_url:

GET https://<host do connect_url>/v1/sse?token=<token>&channel=chat:lobby&channel=alerts&rewind=20
  • Informe cada canal em um channel= (ou vários em channels=a,b), até 50. O token precisa de subscribe em todos.
  • Cada data: é um frame JSON igual ao do WebSocket (connected, attached, message, presence, error); linhas : ping mantêm a conexão viva.
  • O fluxo conta no limite de conexões da conta e termina quando o token expira (antes vem um frame error com realtime.token_invalid) ou quando o nó é reiniciado.
  • Para publicar, use a API ou um WebSocket.
const url = new URL(connectUrl); // o connect_url da resposta do token
const sse = new EventSource(
  `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`,
);
sse.onmessage = (event) => {
  const frame = JSON.parse(event.data);
  if (frame.action === 'message') console.log(frame.data);
};

Presença

A presença diz quem está num canal agora. Um cliente com a capability presence entra com dados livres ({"nick": "ana"}), pode atualizar esses dados e sai ao deixar o canal ou quando a conexão cai. Quem assina o canal recebe os eventos, e get (ou a API) devolve a lista completa.

Frame do clienteExigeEfeito
{"action":"presence","op":"enter","channel":"chat:lobby","data":{…},"id":"p1"}presenceEntra na presença; responde ack.
{"action":"presence","op":"update","channel":"chat:lobby","data":{…}}presenceAtualiza os dados do membro.
{"action":"presence","op":"leave","channel":"chat:lobby"}presenceSai da presença.
{"action":"presence","op":"get","channel":"chat:lobby","id":"p2"}subscribeResponde op: "sync" com todos os members.

Os assinantes recebem {"action":"presence","op":"enter|update|leave","channel":"chat:lobby","member":{"connection_id":"…","client_id":"user-42","data":{…},"timestamp":1790338000000}}. Cada conexão é um membro (o mesmo usuário em duas abas conta duas vezes). Um canal comporta até 200 membros; acima disso, realtime.presence_limit. Se um nó cair sem avisar, os membros dele somem da lista em até 30 segundos.

Listar a presença do canal

GET/v1/tools/realtime/channels/{channel}/presence
Custo
Grátis
Dry-run
Não
Autenticação
qualquer um detools:realtime:subscribetools:realtime:managetools:manage

Devolve os membros do canal neste instante. É útil para mostrar "quem está online" a partir do seu servidor, sem abrir uma conexão.

curl -sS -X GET 'https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/presence' \
  -H "X-API-Token: $RESPAWN_API_TOKEN"
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/presence', {
  method: 'GET',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
  },
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.get(
    "https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/presence",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/presence');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/presence", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 200

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "channel": "chat:lobby",
    "members": [
      {
        "connection_id": "c_8f3a1d",
        "client_id": "user-42",
        "data": {
          "nick": "ana"
        },
        "timestamp": 1790338000000
      }
    ]
  }
}

Reconexão

Conexões caem: o nó reinicia, a rede oscila, o token expira. Toda aplicação precisa reconectar sozinha. O exemplo da seção Conexão pelo navegador já faz isso (o SDK também); as regras são estas e os close codes dizem quando reconectar.

Backoff

Use backoff exponencial com jitter (um valor aleatório até o teto da tentativa) e teto de 30 segundos: por exemplo, 0,5 s, 1 s, 2 s, 4 s… Sem o jitter, milhares de clientes reconectam no mesmo instante depois de uma queda e derrubam o nó de novo. Zere a contagem quando receber connected.

Token novo e retry_after_ms

Peça um token novo a cada reconexão: ele é barato (grátis) e evita a conexão com token vencido. Se a resposta trouxer retry_after_ms, espere esse tempo antes de conectar; ele vale em lugar do backoff daquela tentativa. Use sempre o connect_url da resposta mais recente.

Recuperar mensagens com rewind

Uma conexão nova não sabe o que passou enquanto estava fora. Depois de connected, assine de novo cada canal com rewind: o servidor reenvia as últimas mensagens antes de entregar as novas, e o attached marca o fim da recuperação. Como o rewind pode repetir mensagens que você já exibiu, descarte as que tiverem um id já visto. Entre de novo na presença de cada canal em que estava.

Escolha o rewind pelo tempo que a conexão costuma ficar fora (até 100 mensagens). Se o canal teve mais mensagens que isso durante a queda, a diferença se perdeu: o histórico guarda só as últimas.

Histórico

Cada canal guarda as últimas 100 mensagens publicadas, por até 24 horas. O rewind usa esse histórico; para consultá-lo a partir do seu servidor, use a API.

Consultar histórico do canal

GET/v1/tools/realtime/channels/{channel}/history
Custo
Franquia diária, depois 0,002 crédito/leitura via API
Dry-run
Suportado (X-Dry-Run: true)
Autenticação
qualquer um detools:realtime:subscribetools:realtime:managetools:manage

Devolve as mensagens da mais antiga para a mais nova, em messages (id, name, data, timestamp, client_id), junto de channel e do limit aplicado. Não há cursor nem páginas: limit vale no máximo 100 (o padrão é o máximo). É uma leitura medida: entra na franquia diária de leituras da conta.

curl -sS -X GET 'https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/history?limit=50' \
  -H "X-API-Token: $RESPAWN_API_TOKEN"
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/history?limit=50', {
  method: 'GET',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
  },
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.get(
    "https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/history?limit=50",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/history?limit=50');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/history?limit=50", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 200 — da mais antiga para a mais nova, sem paginação; timestamp em milissegundos Unix

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "channel": "chat:lobby",
    "limit": 100,
    "messages": [
      {
        "id": "1790338000000-0",
        "name": "message",
        "data": {
          "text": "Olá!"
        },
        "timestamp": 1790338000000,
        "client_id": "user-42"
      }
    ]
  }
}

Publicar mensagem pelo servidor

Publicar pela API é útil para eventos que nascem no seu backend (um pedido pago, um alerta). A mensagem chega aos assinantes como qualquer outra.

POST/v1/tools/realtime/channels/{channel}/messages
Custo
Por mensagem publicada, além da franquia mensal
Dry-run
Suportado (X-Dry-Run: true)
Autenticação
qualquer um detools:realtime:publishtools:realtime:managetools:manage
  • data: qualquer JSON, até 64 KB (somados ao name).
  • name (opcional, até 128 caracteres): classifica a mensagem.
  • client_id (opcional): o remetente que os assinantes veem.

A resposta traz id, timestamp (milissegundos Unix) e charged: true quando a mensagem passou da franquia mensal e foi cobrada.

Com X-Dry-Run: true, a API valida o canal e o tamanho, não publica nem cobra, e responde 200 com a estimativa:

{ "success": true, "data": { "cost_units": 0, "within_quota": true, "free_used": 18250, "free_monthly": 200000, "would_charge": false } }

cost_units usa a escala do backend (10.000 unidades = 1 crédito) e é 0 dentro da franquia.

curl -sS -X POST 'https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/messages' \
  -H "X-API-Token: $RESPAWN_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "name": "message", "data": { "text": "Pedido aprovado" }, "client_id": "sistema" }'
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/messages', {
  method: 'POST',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ "name": "message", "data": { "text": "Pedido aprovado" }, "client_id": "sistema" }),
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.post(
    "https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/messages",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    json={ "name": "message", "data": { "text": "Pedido aprovado" }, "client_id": "sistema" },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/messages');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => <<<'JSON'
{ "name": "message", "data": { "text": "Pedido aprovado" }, "client_id": "sistema" }
JSON,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.respawntech.dev/v1/tools/realtime/channels/chat:lobby/messages", strings.NewReader(`{ "name": "message", "data": { "text": "Pedido aprovado" }, "client_id": "sistema" }`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 201 — timestamp em milissegundos Unix; charged é true quando a mensagem passou da franquia mensal e foi cobrada

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "id": "1790338000000-0",
    "timestamp": 1790338000000,
    "charged": false
  }
}

Canais e estatísticas

Listar canais ativos

GET/v1/tools/realtime/channels
Custo
Grátis
Dry-run
Não
Autenticação
qualquer um detools:realtime:managetools:manage

Lista os canais com assinantes ou com uma publicação dentro da janela do histórico, com name, subscribers (a contagem atrasa até 10 segundos) e last_publish_at. Use limit (1 a 200, padrão 50) e offset; a resposta traz total e, quando há mais páginas, next_offset para a chamada seguinte.

curl -sS -X GET 'https://api.respawntech.dev/v1/tools/realtime/channels?limit=50' \
  -H "X-API-Token: $RESPAWN_API_TOKEN"
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/channels?limit=50', {
  method: 'GET',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
  },
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.get(
    "https://api.respawntech.dev/v1/tools/realtime/channels?limit=50",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/channels?limit=50');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.respawntech.dev/v1/tools/realtime/channels?limit=50", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 200 — canais com assinantes ou com uma publicação dentro da janela do histórico; next_offset só vem quando há mais páginas

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "channels": [
      {
        "name": "chat:lobby",
        "subscribers": 12,
        "last_publish_at": "2026-10-06T12:00:00Z"
      }
    ],
    "total": 1
  }
}

Consultar uso e conexões

GET/v1/tools/realtime/stats
Custo
Grátis
Dry-run
Não
Autenticação
qualquer um detools:realtime:managetools:manage

Mostra as connections abertas agora (somadas em todos os nós), os active_channels (com pelo menos um assinante), o month (UTC) e as messages_this_month, junto da franquia: free_monthly_messages e free_remaining.

curl -sS -X GET 'https://api.respawntech.dev/v1/tools/realtime/stats' \
  -H "X-API-Token: $RESPAWN_API_TOKEN"
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/stats', {
  method: 'GET',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
  },
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.get(
    "https://api.respawntech.dev/v1/tools/realtime/stats",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/stats');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.respawntech.dev/v1/tools/realtime/stats", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 200

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "connections": 42,
    "active_channels": 7,
    "month": "2026-10",
    "messages_this_month": 18250,
    "free_monthly_messages": 200000,
    "free_remaining": 181750
  }
}

Webhooks de canais

Cada conta pode ter um webhook de canais, que avisa o seu servidor quando um canal fica ocupado ou vazio, quando alguém entra ou sai da presença e (opcionalmente) a cada mensagem. É uma configuração do Realtime, separada dos webhooks da conta.

Consultar o webhook de canais

GET/v1/tools/realtime/webhook
Custo
Grátis
Dry-run
Não
Autenticação
qualquer um detools:realtime:managetools:manage

Devolve a configuração atual com o secret e a standard_key (o secret no formato whsec_…, para as bibliotecas Standard Webhooks). Sem webhook configurado, responde 404.

curl -sS -X GET 'https://api.respawntech.dev/v1/tools/realtime/webhook' \
  -H "X-API-Token: $RESPAWN_API_TOKEN"
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/webhook', {
  method: 'GET',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
  },
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.get(
    "https://api.respawntech.dev/v1/tools/realtime/webhook",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/webhook');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.respawntech.dev/v1/tools/realtime/webhook", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 200 — sem webhook configurado, responde 404 (not_found)

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "url": "https://example.com/hooks/realtime",
    "events": [
      "channel.occupied",
      "channel.vacated",
      "channel.message",
      "presence.member_added"
    ],
    "channels": [
      "chat:*"
    ],
    "enabled": true,
    "secret": "3f9c1b7a5e2d4c8f90a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b",
    "standard_key": "whsec_M2Y5YzFiN2E1ZTJkNGM4ZjkwYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOWEwYjFjMmQzZTRmNWE2Yg==",
    "updated_at": "2026-10-06T12:00:00Z"
  }
}

Configurar o webhook de canais

PUT/v1/tools/realtime/webhook
Custo
Grátis
Dry-run
Não
Autenticação
qualquer um detools:realtime:managetools:manage
  • url: URL pública http ou https; endereços de rede interna ou privada são recusados.
  • events: ao menos um dos eventos da tabela abaixo.
  • channels: padrões de canal (nome, prefixo com * ou *), até 50. Vazio quer dizer todos os canais, menos channel.message: as mensagens são opt-in e exigem ao menos um padrão.
  • enabled (opcional, padrão true): false pausa as entregas sem apagar a configuração.

A chamada cria o webhook ou substitui o atual, e o secret é mantido. A configuração é lida em cache por até 30 segundos.

curl -sS -X PUT 'https://api.respawntech.dev/v1/tools/realtime/webhook' \
  -H "X-API-Token: $RESPAWN_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "url": "https://example.com/hooks/realtime", "events": ["channel.occupied", "channel.vacated", "channel.message"], "channels": ["chat:*"], "enabled": true }'
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/webhook', {
  method: 'PUT',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ "url": "https://example.com/hooks/realtime", "events": ["channel.occupied", "channel.vacated", "channel.message"], "channels": ["chat:*"], "enabled": true }),
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.put(
    "https://api.respawntech.dev/v1/tools/realtime/webhook",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    json={ "url": "https://example.com/hooks/realtime", "events": ["channel.occupied", "channel.vacated", "channel.message"], "channels": ["chat:*"], "enabled": True },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/webhook');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => <<<'JSON'
{ "url": "https://example.com/hooks/realtime", "events": ["channel.occupied", "channel.vacated", "channel.message"], "channels": ["chat:*"], "enabled": true }
JSON,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"
)

func main() {
	req, err := http.NewRequest("PUT", "https://api.respawntech.dev/v1/tools/realtime/webhook", strings.NewReader(`{ "url": "https://example.com/hooks/realtime", "events": ["channel.occupied", "channel.vacated", "channel.message"], "channels": ["chat:*"], "enabled": true }`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 200 — cria o webhook ou substitui o atual, mantendo o mesmo secret

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "url": "https://example.com/hooks/realtime",
    "events": [
      "channel.occupied",
      "channel.vacated",
      "channel.message",
      "presence.member_added"
    ],
    "channels": [
      "chat:*"
    ],
    "enabled": true,
    "secret": "3f9c1b7a5e2d4c8f90a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b",
    "standard_key": "whsec_M2Y5YzFiN2E1ZTJkNGM4ZjkwYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOWEwYjFjMmQzZTRmNWE2Yg==",
    "updated_at": "2026-10-06T12:00:00Z"
  }
}

Trocar o secret do webhook

POST/v1/tools/realtime/webhook/rotate-secret
Custo
Grátis
Dry-run
Não
Autenticação
qualquer um detools:realtime:managetools:manage

Gera um novo secret (e a standard_key correspondente). O anterior deixa de valer; atualize o seu receptor logo em seguida.

curl -sS -X POST 'https://api.respawntech.dev/v1/tools/realtime/webhook/rotate-secret' \
  -H "X-API-Token: $RESPAWN_API_TOKEN"
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/webhook/rotate-secret', {
  method: 'POST',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
  },
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.post(
    "https://api.respawntech.dev/v1/tools/realtime/webhook/rotate-secret",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/webhook/rotate-secret');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.respawntech.dev/v1/tools/realtime/webhook/rotate-secret", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 200 — o secret anterior deixa de valer; a configuração é lida em cache por até 30 segundos

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "url": "https://example.com/hooks/realtime",
    "events": [
      "channel.occupied",
      "channel.vacated",
      "channel.message",
      "presence.member_added"
    ],
    "channels": [
      "chat:*"
    ],
    "enabled": true,
    "secret": "3f9c1b7a5e2d4c8f90a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b",
    "standard_key": "whsec_M2Y5YzFiN2E1ZTJkNGM4ZjkwYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOWEwYjFjMmQzZTRmNWE2Yg==",
    "updated_at": "2026-10-06T12:00:00Z"
  }
}

Remover o webhook de canais

DELETE/v1/tools/realtime/webhook
Custo
Grátis
Dry-run
Não
Autenticação
qualquer um detools:realtime:managetools:manage

Apaga a configuração; nenhum evento é mais enviado.

curl -sS -X DELETE 'https://api.respawntech.dev/v1/tools/realtime/webhook' \
  -H "X-API-Token: $RESPAWN_API_TOKEN"
const response = await fetch('https://api.respawntech.dev/v1/tools/realtime/webhook', {
  method: 'DELETE',
  headers: {
    'X-API-Token': process.env.RESPAWN_API_TOKEN,
  },
});

if (!response.ok) throw new Error((await response.json()).error.code);
console.log(await response.json());
import os
import requests

response = requests.delete(
    "https://api.respawntech.dev/v1/tools/realtime/webhook",
    headers={
        "X-API-Token": os.environ["RESPAWN_API_TOKEN"],
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(response.json()["error"]["code"])
print(response.json())
<?php
$ch = curl_init('https://api.respawntech.dev/v1/tools/realtime/webhook');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_HTTPHEADER => [
        'X-API-Token: ' . getenv('RESPAWN_API_TOKEN'),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException(json_decode($body, true)['error']['code']);
}
print_r(json_decode($body, true));
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	req, err := http.NewRequest("DELETE", "https://api.respawntech.dev/v1/tools/realtime/webhook", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Token", os.Getenv("RESPAWN_API_TOKEN"))

	client := &http.Client{Timeout: 30 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode >= 400 {
		panic(string(data))
	}
	fmt.Println(string(data))
}

Resposta 200

{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "deleted": true
  }
}

Eventos

EventoQuando dispara
channel.occupiedO primeiro assinante entrou num canal que estava vazio (somando todos os nós).
channel.vacatedO último assinante saiu do canal.
presence.member_addedUm membro entrou na presença de um canal; o `data` traz o `member`.
presence.member_updatedUm membro atualizou os dados da sua presença.
presence.member_removedUm membro saiu da presença, ou a conexão dele caiu.
channel.messageUma mensagem foi publicada num canal. Só chega para os canais listados em `channels` do webhook; o `data` traz a `message`.

Entrega

A plataforma faz um POST JSON na sua URL:

{
  "id": "evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11",
  "type": "presence.member_added",
  "timestamp": 1790338000000,
  "account_id": "7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27",
  "data": {
    "channel": "chat:lobby",
    "member": { "connection_id": "c_8f3a1d", "client_id": "user-42", "data": { "nick": "ana" }, "timestamp": 1790338000000 }
  }
}

data.channel está em todos os eventos; data.member vem nos eventos de presença e data.message (id, name, data, timestamp, client_id) em channel.message. O timestamp é em milissegundos Unix.

  • Assinatura: Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature), com a standard_key da configuração; veja Validar a assinatura.
  • Tentativas: até 3 (esperas de 1 s e de 5 s) em erro de rede, 5xx ou 429; outras respostas encerram a entrega. Cada tentativa espera até 5 segundos.
  • Processamento: a entrega é assíncrona, por 4 trabalhadores; se a fila encher, o evento é descartado. Trate o webhook como um aviso e use o id para descartar duplicatas.

SDK JavaScript

O pacote @respawn/realtime implementa o protocolo acima, sem dependências, para o navegador e o Node 22 ou mais novo. Ele renova o token 30 segundos antes de expirar, reconecta conforme os close codes, retoma os canais com rewind (100 mensagens, sem repetir as já entregues) e entra de novo na presença.

npm install @respawn/realtime

No servidor

RealtimeRest usa a chave de API (nunca a envie ao navegador) para emitir tokens, publicar, ler o histórico e a presença. Passe baseUrl com o endereço da API:

import { RealtimeRest } from '@respawn/realtime';

const rest = new RealtimeRest({
  apiKey: process.env.RESPAWN_API_TOKEN!,
  baseUrl: 'https://api.respawntech.dev',
});

// rota que o navegador chama (Express): devolve token, expires_at e connect_url
app.post('/realtime/token', async (req, res) => {
  res.json(
    await rest.createToken({
      client_id: req.user.id,
      capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] },
      ttl_seconds: 600,
    }),
  );
});

await rest.publish('room:1', 'pedido', { id: 42, status: 'pago' });
const history = await rest.history('room:1', { limit: 20 });
const members = await rest.presence('room:1');

No navegador

Realtime pede o token ao seu servidor por authUrl (um POST com cookies) ou por authCallback:

import { Realtime } from '@respawn/realtime';

const rt = new Realtime({ authUrl: '/realtime/token' });
rt.on('state', (state, err) => console.log('conexão:', state, err?.code));

const room = rt.channel('room:1');

await room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 });
await room.publish('chat', { text: 'oi' });

await room.presence.watch((members) => render(members));
await room.presence.enter({ nick: 'ana' });

Os estados são initialized, connecting, connected, disconnected, closed (você chamou close()) e failed (por exemplo, close 4003). Se o intervalo fora do ar passou da janela do histórico, o canal emite o evento gap. Publicações feitas sem conexão ficam na fila; uma já enviada quando a conexão cai falha com sdk.connection_lost (pode ou não ter sido publicada).

React

Com @respawn/realtime/react:

import { useState } from 'react';
import { Realtime } from '@respawn/realtime';
import { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react';

const rt = new Realtime({ authUrl: '/realtime/token' });

export function App() {
  return (
    <RealtimeProvider client={rt}>
      <Room />
    </RealtimeProvider>
  );
}

function Room() {
  const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]);
  const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 });
  const { members } = usePresence('room:1', { nick: 'ana' });

  return (
    <div>
      <p>{members.length} online</p>
      <ul>{msgs.map((m) => <li key={m.id}>{JSON.stringify(m.data)}</li>)}</ul>
      <button onClick={() => publish('chat', { text: 'oi' })}>Enviar</button>
    </div>
  );
}

Limites

LimiteValor
Mensagens grátis por mês (por conta)200.000
Tamanho de uma mensagem (data)64 KB
Tamanho do nome do canal128 caracteres
Canais assinados por conexão50
Publicações por segundo (por conta)200
Conexões simultâneas (por conta)200
Membros na presença de um canal200
Validade do token1 hora (padrão), de 1 minuto até 24 horas
Padrões de canal por tokenaté 100
Retenção do histórico24 horas
Mensagens guardadas e por rewindaté 100
Canais por página (GET /channels)até 200
Padrões de canal do webhookaté 50

Os valores são os padrões da plataforma e podem ser ajustados por ela. As requisições à API também seguem o limite de requisições da conta.

Erros

Os erros da API REST (realtime.channel_invalid, realtime.message_too_large…) chegam como resposta HTTP, no formato de Erros. Na conexão WebSocket ou SSE, os mesmos códigos chegam por um frame error (a conexão continua aberta) ou por um close code (veja Protocolo).

CódigoHTTPQuando
realtime.disabled403Realtime ainda não liberado para a conta (na conexão WebSocket, close 4003).
realtime.token_invalid401Token de conexão inválido ou expirado; na conexão WebSocket, close 4001. Emita um token novo.
realtime.capability_denied403O token não permite a operação (subscribe, publish ou presence) no canal; no WebSocket, frame error com channel e a conexão continua aberta.
realtime.channel_invalid400Nome de canal ou padrão de canal inválido: vazio, com mais de 128 caracteres ou com caracteres fora de A-Z a-z 0-9 : _ - ..
realtime.message_too_large413Mensagem (data) acima de 64 KB.
realtime.rate_limited429A conta passou do limite de publicações por segundo; a mensagem é descartada e, no WebSocket, a conexão continua aberta.
realtime.connection_limit429A conta atingiu o limite de conexões simultâneas (WebSocket: close 4029). Reconecte com backoff.
realtime.presence_limit429O canal já tem o máximo de membros na presença (200).
realtime.insufficient_credits402Saldo insuficiente para publicar uma mensagem além da franquia mensal.
realtime.session_required403A rota só aceita a sessão do dashboard (POST /inspector-token), não uma chave de API.

Erros de validação comuns (validation.failed, not_found) estão em Erros.

Preço

Por mensagem — Conta cada mensagem publicada, pela API ou por uma conexão WebSocket, uma única vez, não importa quantos clientes a recebam. Emitir tokens, conectar, assinar e usar a presença são grátis.

ItemCréditos
Mensagem além da franquia mensal0,001

Franquia mensal — Por conta e por mês (UTC). O valor padrão pode ser ajustado pela plataforma; a resposta da publicação mostra se a mensagem foi cobrada (`charged`).

ItemGrátis por mês
Mensagens200.000

A cobrança é por mensagem publicada, uma vez, pelas mensagens que passam da franquia mensal; o número de assinantes não muda o custo. Emitir tokens, manter conexões abertas, usar a presença, listar canais e configurar o webhook são grátis. A consulta ao histórico é uma leitura medida: entra na franquia diária de leituras e, depois dela, custa o valor da tabela de leituras. Sem saldo para uma mensagem fora da franquia, a publicação falha com realtime.insufficient_credits.

Atualizado em