# Realtime



<ToolHero tool="realtime" />

## 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é <Limit name="realtimeChannelNameMax" /> 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:

<Steps>
  <Step>
    O navegador pede um token ao **seu servidor**, que já sabe quem é o usuário.
  </Step>

  <Step>
    O seu servidor [emite o token](#realtime-token) na API com as capabilities daquele usuário e devolve ao navegador a resposta inteira: `token`, `expires_at` e `connect_url`.
  </Step>

  <Step>
    O navegador conecta em `connect_url`, assina canais e publica.
  </Step>

  <Step>
    Se a conexão cair, o navegador pede um token novo e [reconecta](#reconexao), recuperando as mensagens perdidas com `rewind`.
  </Step>
</Steps>

<Callout type="warn" title="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.
</Callout>

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.

<EndpointList tool="realtime" />

## Token e capabilities

### Emitir token de conexão

<Endpoint id="realtime.token" example={{ body: `{ "client_id": "user-42", "capabilities": { "chat:*": ["subscribe", "publish", "presence"], "alerts": ["subscribe"] }, "ttl_seconds": 3600 }` }}>
  * `capabilities` (obrigatório): objeto de padrão de canal para a lista de operações permitidas, até <Limit name="realtimeTokenPatternsMax" /> 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é <Limit name="realtimeClientIdMax" /> 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 <Limit name="realtimeTokenTtlMin" /> até <Limit name="realtimeTokenTtlMax" />, padrão <Limit name="realtimeTokenTtlDefault" />. 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**.
</Endpoint>

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.

<Callout title="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`.
</Callout>

## 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.

<Tabs groupId="code-language" persist items={['Node.js', 'Python']}>
  <Tab value="Node.js">
    ```js
    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);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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
    ```
  </Tab>
</Tabs>

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](#sdk).

```html
<!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é <Limit name="realtimeRewindMax" />), 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](#presenca): `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](#erros). A conexão continua aberta.                          |

Exemplo de troca:

```json
← { "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`:

```text
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é <Limit name="realtimeChannelsPerConnection" />. 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](#publicar) ou um WebSocket.

```js
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é <Limit name="realtimePresenceMembers" /> 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

<Endpoint id="realtime.presence" example={{ pathParams: { channel: 'chat:lobby' } }}>
  Devolve os membros do canal neste instante. É útil para mostrar "quem está online" a partir do seu servidor, sem abrir uma conexão.
</Endpoint>

## 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](#conexao-pelo-navegador) já faz isso (o [SDK](#sdk) também); as regras são estas e os [close codes](#protocolo) dizem quando reconectar.

### Backoff

Use backoff exponencial **com jitter** (um valor aleatório até o teto da tentativa) e teto de <Limit name="realtimeBackoffMax" />: 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é <Limit name="realtimeRewindMax" /> 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 <Limit name="realtimeHistoryPageMax" /> mensagens publicadas, por até <Limit name="realtimeHistoryRetention" />. O `rewind` usa esse histórico; para consultá-lo a partir do seu servidor, use a API.

### Consultar histórico do canal

<Endpoint id="realtime.history" example={{ pathParams: { channel: 'chat:lobby' }, query: { limit: '50' } }}>
  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 <Limit name="realtimeHistoryPageMax" /> (o padrão é o máximo). É uma leitura medida: entra na [franquia diária de leituras](/cobranca-e-limites#franquia-diaria) da conta.
</Endpoint>

### 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.

<Endpoint id="realtime.publish" example={{ pathParams: { channel: 'chat:lobby' }, body: `{ "name": "message", "data": { "text": "Pedido aprovado" }, "client_id": "sistema" }` }}>
  * `data`: qualquer JSON, até <Limit name="realtimeMessageMax" /> (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:

  ```json
  { "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.
</Endpoint>

## Canais e estatísticas

### Listar canais ativos

<Endpoint id="realtime.channels" example={{ query: { limit: '50' } }}>
  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 <Limit name="realtimeChannelListPageMax" />, padrão 50) e `offset`; a resposta traz `total` e, quando há mais páginas, `next_offset` para a chamada seguinte.
</Endpoint>

### Consultar uso e conexões

<Endpoint id="realtime.stats">
  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`.
</Endpoint>

## 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](/webhooks).

### Consultar o webhook de canais

<Endpoint id="realtime.webhook.get">
  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`.
</Endpoint>

### Configurar o webhook de canais

<Endpoint id="realtime.webhook.put" example={{ body: `{ "url": "https://example.com/hooks/realtime", "events": ["channel.occupied", "channel.vacated", "channel.message"], "channels": ["chat:*"], "enabled": true }` }}>
  * `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é <Limit name="realtimeWebhookChannelsMax" />. 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.
</Endpoint>

### Trocar o secret do webhook

<Endpoint id="realtime.webhook.rotate">
  Gera um novo `secret` (e a `standard_key` correspondente). O anterior deixa de valer; atualize o seu receptor logo em seguida.
</Endpoint>

### Remover o webhook de canais

<Endpoint id="realtime.webhook.delete">
  Apaga a configuração; nenhum evento é mais enviado.
</Endpoint>

### Eventos

<WebhookEventsTable source="realtime" />

### Entrega

A plataforma faz um `POST` JSON na sua URL:

```json
{
  "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](/webhooks#verificar-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é <Limit name="realtimeWebhookTimeout" />.
* **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](#protocolo), retoma os canais com `rewind` (100 mensagens, sem repetir as já entregues) e entra de novo na presença.

```bash
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:

```ts
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`:

```ts
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`:

```tsx
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) | <Limit name="realtimeFreeMessages" />                                                                                               |
| Tamanho de uma mensagem (`data`)     | <Limit name="realtimeMessageMax" />                                                                                                 |
| Tamanho do nome do canal             | <Limit name="realtimeChannelNameMax" /> caracteres                                                                                  |
| Canais assinados por conexão         | <Limit name="realtimeChannelsPerConnection" />                                                                                      |
| Publicações por segundo (por conta)  | <Limit name="realtimePublishPerSecond" />                                                                                           |
| Conexões simultâneas (por conta)     | <Limit name="realtimeConnectionsPerAccount" />                                                                                      |
| Membros na presença de um canal      | <Limit name="realtimePresenceMembers" />                                                                                            |
| Validade do token                    | <Limit name="realtimeTokenTtlDefault" /> (padrão), de <Limit name="realtimeTokenTtlMin" /> até <Limit name="realtimeTokenTtlMax" /> |
| Padrões de canal por token           | até <Limit name="realtimeTokenPatternsMax" />                                                                                       |
| Retenção do histórico                | <Limit name="realtimeHistoryRetention" />                                                                                           |
| Mensagens guardadas e por `rewind`   | até <Limit name="realtimeHistoryPageMax" />                                                                                         |
| Canais por página (`GET /channels`)  | até <Limit name="realtimeChannelListPageMax" />                                                                                     |
| Padrões de canal do webhook          | até <Limit name="realtimeWebhookChannelsMax" />                                                                                     |

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](/cobranca-e-limites) da conta.

## Erros

Os erros da API REST (`realtime.channel_invalid`, `realtime.message_too_large`…) chegam como resposta HTTP, no formato de [Erros](/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](#protocolo)).

<ErrorTable tool="realtime" />

Erros de validação comuns (`validation.failed`, `not_found`) estão em [Erros](/erros).

## Preço

<PricingTable tool="realtime" group="per-message" />

<PricingTable tool="realtime" group="free-quota" />

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](/cobranca-e-limites#franquia-diaria). Sem saldo para uma mensagem fora da franquia, a publicação falha com `realtime.insufficient_credits`.
