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:
dataJSON publicada num canal, comid,timestamp(milissegundos Unix) e oclient_idde 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ção | Rota |
|---|---|
| Emitir token de conexão | POST/tokens |
| Publicar mensagem pelo servidor | POST/channels/{channel}/messages |
| Consultar histórico do canal | GET/channels/{channel}/history |
| Listar a presença do canal | GET/channels/{channel}/presence |
| Listar canais ativos | GET/channels |
| Consultar uso e conexões | GET/stats |
| Consultar o webhook de canais | GET/webhook |
| Configurar o webhook de canais | PUT/webhook |
| Trocar o secret do webhook | POST/webhook/rotate-secret |
| Remover o webhook de canais | DELETE/webhook |
Token e capabilities
Emitir token de conexão
/v1/tools/realtime/tokens- Custo
- Grátis
- Dry-run
- Não
- Autenticação
- qualquer um de
tools: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ãosubscribe,publish,presence(entrar na presença; ler a presença exigesubscribe) ou*(todas). Sem capability para o canal, a operação falha comrealtime.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_codeNo 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
action | Campos | O que faz |
|---|---|---|
subscribe | channel, 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. |
unsubscribe | channel | Deixa de receber o canal. Responde detached. |
publish | channel, name?, data, id? | Publica data (JSON) no canal (exige publish). name classifica a mensagem. Se enviar id, o servidor responde ack com ele. |
presence | op, channel, data?, id? | Operações de presença: op é enter, update, leave ou get. |
Do servidor para o cliente
action | Campos | Quando |
|---|---|---|
connected | connection_id | A conexão foi aceita; já pode assinar. |
attached | channel | A assinatura do canal está ativa (depois do rewind, se pedido). |
detached | channel | A assinatura foi encerrada. |
message | channel, id, name?, data, timestamp, client_id? | Uma mensagem do canal, inclusive as que você mesmo publicou. timestamp é um inteiro em milissegundos Unix. |
ack | id, channel? | A publicação (ou operação de presença) com esse id foi aceita. |
presence | op, channel, member ou members | Evento de presença (enter, update, leave) ou resposta ao get (sync). |
error | code, 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ódigo | Significado | O que fazer |
|---|---|---|
1012 | O nó está reiniciando (deploy ou manutenção). | Reconecte já, com um pequeno jitter. |
1013 | O nó está cheio. | Reconecte com backoff exponencial. |
4001 | Token inválido ou expirado (realtime.token_invalid). | Emita um token novo e reconecte; repetir o mesmo token não adianta. |
4003 | Realtime desativado para a conta (realtime.disabled). | Não reconecte. |
4029 | A conta atingiu o limite de conexões (realtime.connection_limit). | Reconecte com backoff exponencial. |
| Qualquer outro | Queda 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 emchannels=a,b), até 50. O token precisa desubscribeem todos. - Cada
data:é um frame JSON igual ao do WebSocket (connected,attached,message,presence,error); linhas: pingmantê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
errorcomrealtime.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 cliente | Exige | Efeito |
|---|---|---|
{"action":"presence","op":"enter","channel":"chat:lobby","data":{…},"id":"p1"} | presence | Entra na presença; responde ack. |
{"action":"presence","op":"update","channel":"chat:lobby","data":{…}} | presence | Atualiza os dados do membro. |
{"action":"presence","op":"leave","channel":"chat:lobby"} | presence | Sai da presença. |
{"action":"presence","op":"get","channel":"chat:lobby","id":"p2"} | subscribe | Responde 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
/v1/tools/realtime/channels/{channel}/presence- Custo
- Grátis
- Dry-run
- Não
- Autenticação
- qualquer um de
tools: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
/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 de
tools: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.
/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 de
tools:realtime:publishtools:realtime:managetools:manage
data: qualquer JSON, até 64 KB (somados aoname).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
/v1/tools/realtime/channels- Custo
- Grátis
- Dry-run
- Não
- Autenticação
- qualquer um de
tools: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
/v1/tools/realtime/stats- Custo
- Grátis
- Dry-run
- Não
- Autenticação
- qualquer um de
tools: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
/v1/tools/realtime/webhook- Custo
- Grátis
- Dry-run
- Não
- Autenticação
- qualquer um de
tools: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
/v1/tools/realtime/webhook- Custo
- Grátis
- Dry-run
- Não
- Autenticação
- qualquer um de
tools:realtime:managetools:manage
url: URL públicahttpouhttps; 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, menoschannel.message: as mensagens são opt-in e exigem ao menos um padrão.enabled(opcional, padrãotrue):falsepausa 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
/v1/tools/realtime/webhook/rotate-secret- Custo
- Grátis
- Dry-run
- Não
- Autenticação
- qualquer um de
tools: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
/v1/tools/realtime/webhook- Custo
- Grátis
- Dry-run
- Não
- Autenticação
- qualquer um de
tools: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
| Evento | Quando dispara |
|---|---|
channel.occupied | O primeiro assinante entrou num canal que estava vazio (somando todos os nós). |
channel.vacated | O último assinante saiu do canal. |
presence.member_added | Um membro entrou na presença de um canal; o `data` traz o `member`. |
presence.member_updated | Um membro atualizou os dados da sua presença. |
presence.member_removed | Um membro saiu da presença, ou a conexão dele caiu. |
channel.message | Uma 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 astandard_keyda configuração; veja Validar a assinatura. - Tentativas: até 3 (esperas de 1 s e de 5 s) em erro de rede,
5xxou429; 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
idpara 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/realtimeNo 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
| Limite | Valor |
|---|---|
| Mensagens grátis por mês (por conta) | 200.000 |
Tamanho de uma mensagem (data) | 64 KB |
| Tamanho do nome do canal | 128 caracteres |
| Canais assinados por conexão | 50 |
| Publicações por segundo (por conta) | 200 |
| Conexões simultâneas (por conta) | 200 |
| Membros na presença de um canal | 200 |
| Validade do token | 1 hora (padrão), de 1 minuto até 24 horas |
| Padrões de canal por token | até 100 |
| Retenção do histórico | 24 horas |
Mensagens guardadas e por rewind | até 100 |
Canais por página (GET /channels) | até 200 |
| Padrões de canal do webhook | até 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ódigo | HTTP | Quando |
|---|---|---|
realtime.disabled | 403 | Realtime ainda não liberado para a conta (na conexão WebSocket, close 4003). |
realtime.token_invalid | 401 | Token de conexão inválido ou expirado; na conexão WebSocket, close 4001. Emita um token novo. |
realtime.capability_denied | 403 | O 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_invalid | 400 | Nome 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_large | 413 | Mensagem (data) acima de 64 KB. |
realtime.rate_limited | 429 | A conta passou do limite de publicações por segundo; a mensagem é descartada e, no WebSocket, a conexão continua aberta. |
realtime.connection_limit | 429 | A conta atingiu o limite de conexões simultâneas (WebSocket: close 4029). Reconecte com backoff. |
realtime.presence_limit | 429 | O canal já tem o máximo de membros na presença (200). |
realtime.insufficient_credits | 402 | Saldo insuficiente para publicar uma mensagem além da franquia mensal. |
realtime.session_required | 403 | A 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.
| Item | Créditos |
|---|---|
| Mensagem além da franquia mensal | 0,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`).
| Item | Grátis por mês |
|---|---|
| Mensagens | 200.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