# Inbox — conversas e mensagens (/docs/crm/inbox)





O inbox é o **sistema de registro** das conversas: todo o histórico de mensagens, a mídia hospedada e os metadados de cada contato ficam acessíveis pela API `/v1/inbox/*`, independente de qual instância (número) recebeu cada mensagem.

<Callout type="warn">
  As conversas **não** ficam sob a instância. Não procure por `/instances/{id}/conversations&#x60; — o
  caminho é &#x2A;*`/v1/inbox/conversations`**. A instância é apenas o número que enviou/recebeu; o
  histórico é centralizado no inbox.
</Callout>

<Callout type="warn">
  **As conversas seguem o modo (teste/produção) da instância** — é o mesmo [isolamento de dados por
  modo](/docs/get-started/authentication#isolamento-de-dados-por-modo) das instâncias e webhooks. A
  conversa de uma instância de **teste** só aparece pela **API de teste**
  (`sandbox.pepinozap.com.br` + key `pzk_test_`); o **Inbox do painel é sempre Produção**, então
  conversa de instância de teste **não aparece lá** — não é bug, é o modo. Para vê-la no painel, use
  uma instância de **produção** (key `pzk_live_` / host `api.*`).
</Callout>

## Listar conversas [#listar-conversas]

[`GET /v1/inbox/conversations`](/docs/api/inbox/listInboxConversations) devolve as conversas com o **resumo** (último trecho, não-lidas, status, etiquetas, atendente/departamento atribuído) — **sem** o corpo das mensagens. Aceita filtros por `status` (`open`/`pending`/`resolved`), `label` e `department`, e ecoa os **contadores** por status para os badges das abas.

Use essa lista para a coluna da esquerda do seu inbox e re-busque-a periodicamente (ou por evento de WebSocket) para manter os badges e o último trecho atualizados.

## Carregar as mensagens de uma conversa [#carregar-as-mensagens-de-uma-conversa]

[`GET /v1/inbox/conversations/{id}/messages`](/docs/api/inbox/listInboxMessages) devolve a **leva mais recente** de mensagens da conversa (texto, mídia, direção, timestamps, status de entrega). Para o histórico mais antigo:

| Como                        | Endpoint                                                                                  |
| --------------------------- | ----------------------------------------------------------------------------------------- |
| Paginar para trás           | `GET .../messages?before=<messageId>` (mais antigas que aquela mensagem)                  |
| Puxar histórico do WhatsApp | [`POST /v1/inbox/conversations/{id}/history`](/docs/api/inbox/requestConversationHistory) |

<Callout type="info">
  Cada mensagem tem uma **direção** (`inbound`/`outbound`). O `outbound` inclui tanto o que sua
  integração/painel envia quanto o que a equipe responde **direto no celular** — o número é
  multi-device, então o que sai de qualquer aparelho é espelhado aqui (e conta no tempo de resposta
  dos relatórios). O webhook [`message.received`](/docs/webhooks/events) dispara &#x2A;*só para
  `inbound`**; mensagem que sai não gera `message.received`.
</Callout>

## Padrões para um inbox responsivo [#padrões-para-um-inbox-responsivo]

A diferença entre um inbox que "abre na hora" e um que pisca a cada clique está no **cliente**, não no endpoint. Quatro padrões que valem a pena seguir:

### 1. Cache por conversa (stale-while-revalidate) [#1-cache-por-conversa-stale-while-revalidate]

Não re-busque do zero (com skeleton) toda vez que o usuário abre uma conversa que **já viu nesta sessão**. Guarde as mensagens por `conversationId` no cliente:

* **Ao abrir:** se há cache para aquela conversa, mostre-o **na hora** (sem skeleton) e dispare o fetch em background para revalidar — atualize a tela se algo mudou.
* **Skeleton só na primeira** abertura de cada conversa (cold load).

Isso mantém a navegação entre conversas instantânea e evita o "pisca-skeleton" a cada clique. Limpar tudo e mostrar skeleton em **toda** abertura é a causa nº 1 de inbox que "ficou lento".

### 2. Preserve as URLs de mídia [#2-preserve-as-urls-de-mídia]

A `mediaUrl` de cada mensagem é uma URL **presigned, reassinada a cada resposta** (e que expira). Se você trocar o `src` de uma `<img>`/`<video>` a cada revalidação, o navegador **recarrega a mídia** — o vídeo trava reiniciando do zero.

Guarde a URL da **primeira** vez que carregou cada mensagem (por `messageId`) e reuse-a nas revalidações; só mensagens **novas** usam a URL fresca da resposta.

<Callout type="info">
  Porque a URL presigned **expira**, ela não serve como link durável. Para um link compartilhável ou
  para reter a mídia por conta própria, **rehospede** o arquivo do seu lado a partir do primeiro
  download. Veja [Persistência](/docs/get-started/persistencia).
</Callout>

### 3. Mantenha ao vivo — polling ou WebSocket [#3-mantenha-ao-vivo--polling-ou-websocket]

* **Polling:** re-busque as mensagens da conversa **aberta** a cada \~5s e a lista de conversas a cada \~10s. Simples e suficiente para a maioria dos casos.
* **WebSocket (push):** menor latência — em vez de esperar o próximo tick (num polling de 5s são \~2.5s de atraso médio), o evento `message.received` chega em **menos de 100ms** e você recarrega na hora. Veja [Tempo real por WebSocket](/docs/webhooks/websocket).

**Padrão recomendado (o que o próprio painel da PEPINO ZAP usa):*&#x2A; WebSocket para o push instantâneo &#x2A;*+** um polling lento como **rede de segurança**. O WS entrega a latência baixa; o polling (mais espaçado) garante que nada se perca se a conexão cair entre um evento e a reconexão. Na prática: no `message.received`, recarregue a conversa aberta e a lista; mantenha um `setInterval` de fallback; e, num front **multi-usuário**, conecte o WS com o [token efêmero](/docs/webhooks/websocket#token-efemero-para-o-navegador), não com a API key.

Os dois combinam bem com o cache do item 1: o evento (ou o tick do polling) revalida a conversa aberta; o resto continua servido do cache.

### 4. Role para o fim ao abrir [#4-role-para-o-fim-ao-abrir]

Ao abrir uma conversa, role para o fim de forma **instantânea** (não suave) e **re-cole no fim** conforme cada mídia carrega:

* Imagem e vídeo **mudam de altura depois de carregar** e empurram o conteúdo para baixo — uma rolagem disparada cedo "para no meio" quando a mídia chega.
* **Re-cole no fim com um `ResizeObserver`** no container das mensagens, **não** com o `onload` de cada mídia. Ao reabrir uma conversa, o cache reusa a **mesma URL** de mídia → o navegador serve do **cache dele** e o evento `load` pode disparar **antes** do seu handler ser anexado, perdendo a re-rolagem (a conversa trava "no meio"). O `ResizeObserver` pega **qualquer** mudança de altura, independente do `onload`.
* Só auto-role se o usuário estiver **colado no fim** (distância até a última mensagem abaixo de um limiar, ex. 120px). Se ele rolou para cima lendo o histórico, **não o arranque** do lugar.

<Callout type="info">
  Combinado com o cache do item 1, reabrir uma conversa já vista é instantâneo **e** já no fim — sem
  skeleton e sem a rolagem visível "descendo até a última mensagem".
</Callout>
