# Persistência e histórico (/docs/get-started/persistencia)





O PEPINO ZAP **persiste todo o histórico** das suas conversas (texto e mídia) na nossa infraestrutura. Para o integrador isso significa uma coisa direta: **você não precisa manter um banco de dados nem um storage de mídia próprios** só para o WhatsApp. Nós somos o *system of record*; você lê o histórico pela API quando precisar.

Esta página explica **o que é guardado**, a diferença entre **histórico persistido e efêmero**, **como ler** o histórico pela API e o **impacto no Inbox/conversas**. Os exemplos usam a URL de produção `https://api.pepinozap.com.br`; no modo teste, troque para `https://sandbox.pepinozap.com.br` (com uma key `pzk_test_`). Todas as requisições exigem o header `Authorization: Bearer <API_KEY>`, e a key precisa **casar com o host** — ver [Autenticação](/docs/get-started/authentication#modos-teste-e-producao).

## O que guardamos [#o-que-guardamos]

Tudo que passa pelo seu número conectado é gravado e fica vinculado ao seu **Projeto**, escopado por modo (teste/produção):

| Dado                                                      | Onde fica          | Você precisa guardar? |
| --------------------------------------------------------- | ------------------ | --------------------- |
| Conversas (uma por contato)                               | Nosso Postgres     | Não                   |
| Mensagens (enviadas e recebidas)                          | Nosso Postgres     | Não                   |
| Contatos                                                  | Nosso Postgres     | Não                   |
| Mídia recebida (imagem, áudio, vídeo, documento, sticker) | Nosso storage (S3) | Não                   |

<Callout type="info">
  **Teste e produção são separados.** O histórico de uma key `pzk_test_` (host `sandbox.*`) e o de
  uma key `pzk_live_` (host `api.*`) vivem em espaços isolados. Uma conversa criada em teste **não**
  aparece nas consultas de produção, e vice-versa. Veja [Modos: teste e
  produção](/docs/get-started/authentication#modos-teste-e-producao).
</Callout>

<Callout type="warn">
  **Retenção de mídia: 2 anos.** O **texto** das mensagens fica no histórico indefinidamente; o
  **binário da mídia** (imagem, áudio, vídeo, documento, sticker) é guardado por **2 anos** e,
  depois disso, é **removido automaticamente** da nossa infraestrutura — a mensagem permanece (texto
  e metadados), mas sem o arquivo. Se você precisa da mídia por mais tempo, **baixe e armazene no
  seu lado** quando o evento chega (a `mediaUrl` é pré-assinada e expira; ver
  [Eventos](/docs/webhooks/events)).
</Callout>

## O histórico sobrevive a deletar a instância [#o-histórico-sobrevive-a-deletar-a-instância]

Apagar (ou desconectar) uma instância remove apenas a **sessão do WhatsApp** daquele número — o **histórico permanece**:

| Ação                        | O que acontece                                                                                                                                               |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Desconectar** a instância | Nada é apagado. A instância e a sessão ficam; reconecta sozinha. Histórico intacto.                                                                          |
| **Deletar** a instância     | Remove a sessão (pareamento) do WhatsApp. As **conversas, mensagens e contatos permanecem** — eles são vinculados ao **Projeto + contato**, não à instância. |

Como as conversas são chaveadas pelo &#x2A;*contato (número)**, ao criar uma instância nova e parear o mesmo número, o histórico daquele número continua disponível pela API. Para **enviar** mensagens você precisa de uma instância conectada; para **ler** o histórico, não.

<Callout type="info">
  Mesmo depois de deletar a instância de origem, a conversa e suas mensagens **continuam
  acessíveis** — eles são chaveados pelo **Projeto + contato**, não pela instância. O `instanceId`
  que a conversa carrega é apenas a referência de por onde ela entrou; deletar a instância não apaga
  o histórico.
</Callout>

## Persistido vs. efêmero [#persistido-vs-efêmero]

Cada Projeto tem um modo de histórico. O padrão — e o recomendado para a maioria das integrações — é **persistido**. Existe também um modo **efêmero**, usado em cenários específicos (por exemplo, números de atendimento onde só interessa o que está em aberto *agora*, sem manter um arquivo durável visível no Inbox).

| Modo                    | O que você vê no Inbox                                                                                                                                                                            | Os dados são apagados? |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| **Persistido** (padrão) | **Todas** as conversas do Projeto, sempre — independentemente de a instância estar conectada ou ter sido deletada.                                                                                | Não.                   |
| **Efêmero**             | **Apenas*&#x2A; as conversas cuja instância está &#x2A;*`CONNECTED`** no momento da consulta. Ao desconectar a instância, as conversas dela **somem da listagem**; ao reconectar, **reaparecem**. | Não.                   |

<Callout type="warn">
  **Efêmero NÃO apaga nada.** As mensagens e conversas continuam gravadas no nosso banco em ambos os
  modos — o que muda é só **o que aparece na listagem do Inbox**. No modo efêmero, conversas de
  instâncias não-conectadas ficam *ocultas* enquanto a instância está fora; voltam a aparecer assim
  que ela fica `CONNECTED` de novo. Não é exclusão, é um filtro de visibilidade.
</Callout>

O estado `CONNECTED` é o da instância pareada e operacional — ver o [ciclo de vida da instância](/docs/get-started/first-instance#ciclo-de-vida). Qualquer outro estado (`DISCONNECTED`, `CONNECTING`, `QR_PENDING`, `BANNED`, `ERROR`) faz, no modo efêmero, as conversas daquela instância saírem da listagem até a reconexão.

### O que muda na prática [#o-que-muda-na-prática]

* **Listagem de conversas** (`GET /v1/inbox/conversations`) e **contadores das abas** (todas/abertas/pendentes/encerradas): no modo efêmero, ambos refletem **só** as conversas de instâncias `CONNECTED`. No persistido, refletem tudo.
* **Detalhe e mensagens de uma conversa** (`GET /v1/inbox/conversations/{id}` e `/messages`): respondem normalmente desde que você tenha o `id` da conversa — esses endpoints não aplicam o filtro de modo. Em outras palavras, mesmo no modo efêmero, se você guardou o `id`, ainda lê o histórico daquela conversa específica.
* **Webhooks e WebSocket**: o modo de histórico **não** muda a entrega de eventos em tempo real. Mensagens recebidas continuam disparando `message.received` normalmente nos dois modos.

### Qual é o padrão e como alterar [#qual-é-o-padrão-e-como-alterar]

O modo padrão de **todo Projeto novo é persistido**. Em caso de erro transitório ao carregar a configuração do Projeto, a API assume **persistido** por segurança — para nunca esconder histórico por engano.

O modo de histórico é uma **configuração do Projeto** definida na criação dele. **Não há endpoint público** (key `pzk_live_`/`pzk_test_`) para o integrador alternar entre persistido e efêmero — não é um liga/desliga self-service como as [configurações da instância](/docs/get-started/first-instance#configuracoes-da-instancia). Para revisar ou mudar o modo do seu Projeto, fale com o suporte.

<Callout type="info">
  Para a esmagadora maioria das integrações, **persistido** (o padrão) é o que você quer: o Inbox
  vira um arquivo durável e você não precisa de banco próprio. Só considere o efêmero se o seu caso
  de uso for explicitamente "mostrar apenas o que está vivo agora".
</Callout>

## Como ler o histórico [#como-ler-o-histórico]

Use os endpoints de **Inbox** (autenticados pela mesma API key). Eles devolvem o histórico persistido, paginado, escopado ao seu Projeto e ao modo (teste/produção) da key. O Inbox lista **apenas conversas 1-a-1** — grupos (`@g.us`), listas de transmissão (`@broadcast`) e canais (`@newsletter`) não aparecem.

### Listar conversas [#listar-conversas]

`GET /v1/inbox/conversations` devolve uma conversa por contato, ordenada por atividade (mais recente primeiro), **30 por página**. Aceita `?status=` (`open`, `pending` ou `resolved`; vazio = todas) e `?page=` (padrão `1`).

```bash
curl "https://api.pepinozap.com.br/v1/inbox/conversations" \
  -H "Authorization: Bearer $YOUR_API_KEY"

# filtrando por status e paginando
curl "https://api.pepinozap.com.br/v1/inbox/conversations?status=open&page=2" \
  -H "Authorization: Bearer $YOUR_API_KEY"
```

Resposta `200 OK`. O bloco `counts` traz os totais por status **de todo o Projeto** (não só da página/filtro atual), úteis para os contadores das abas:

```json
{
  "items": [
    {
      "id": "conv_abc123",
      "instanceId": "inst_xyz789",
      "protocol": "2026-000042",
      "contactId": "cont_def456",
      "contactName": "Maria Silva",
      "contactPhone": "5511999999999",
      "contactJid": "5511999999999@s.whatsapp.net",
      "status": "open",
      "assignedUserId": null,
      "unread": 2,
      "lastMessageAt": "2026-06-05T13:40:00Z",
      "lastMessageText": "Perfeito, obrigado!"
    }
  ],
  "page": 1,
  "counts": {
    "all": 37,
    "open": 12,
    "pending": 5,
    "resolved": 20
  }
}
```

| Campo                               | Significado                                                                                                                                                                                                                                 |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                | Identificador da conversa. Guarde-o para ler as mensagens (abaixo).                                                                                                                                                                         |
| `instanceId`                        | Instância pela qual a conversa entrou. Pode ser `null` em conversas que não estão vinculadas a uma instância. A conversa permanece acessível mesmo que a instância referenciada tenha sido deletada (o histórico não depende da instância). |
| `protocol`                          | Número de protocolo do ticket no Projeto, no formato `ano-sequência` (`null` se não houver).                                                                                                                                                |
| `contactName`                       | Nome do contato. Pode ser `null` se ainda não foi resolvido/definido.                                                                                                                                                                       |
| `contactPhone` / `contactJid`       | Número e JID do WhatsApp do contato.                                                                                                                                                                                                        |
| `status`                            | `open`, `pending` ou `resolved`.                                                                                                                                                                                                            |
| `assignedUserId`                    | Atendente atribuído (`null` se não atribuída).                                                                                                                                                                                              |
| `unread`                            | Mensagens não-lidas.                                                                                                                                                                                                                        |
| `lastMessageAt` / `lastMessageText` | Carimbo e prévia da última mensagem (ambos podem ser `null` em conversa sem mensagens).                                                                                                                                                     |

### Ler as mensagens de uma conversa [#ler-as-mensagens-de-uma-conversa]

`GET /v1/inbox/conversations/{id}/messages` devolve as mensagens em **ordem cronológica*&#x2A; (mais antiga → mais nova), em páginas. Por padrão traz as **`limit` mais recentes** (`limit` padrão `50`, máximo `200`). Para o histórico completo (scrollback), pagine **para trás** com o cursor `before`.

```bash
# página mais recente (até 50 mensagens)
curl "https://api.pepinozap.com.br/v1/inbox/conversations/conv_abc123/messages" \
  -H "Authorization: Bearer $YOUR_API_KEY"

# página anterior (mais antigas): use o nextBefore da resposta anterior
curl "https://api.pepinozap.com.br/v1/inbox/conversations/conv_abc123/messages?before=msg_000123&limit=100" \
  -H "Authorization: Bearer $YOUR_API_KEY"
```

Resposta `200 OK`:

```json
{
  "items": [
    {
      "id": "msg_000122",
      "direction": "INBOUND",
      "type": "text",
      "text": "Bom dia, tudo bem?",
      "mediaUrl": null,
      "mediaMime": null,
      "status": "READ",
      "waMessageId": "3EB0XXXXXXXXXXXX",
      "quotedWaMessageId": null,
      "createdAt": "2026-06-05T13:38:00Z"
    },
    {
      "id": "msg_000123",
      "direction": "OUTBOUND",
      "type": "image",
      "text": "Segue a tabela",
      "mediaUrl": "https://s3.amazonaws.com/.../arquivo.jpg?X-Amz-Signature=...",
      "mediaMime": "image/jpeg",
      "status": "DELIVERED",
      "waMessageId": "3EB0YYYYYYYYYYYY",
      "quotedWaMessageId": null,
      "createdAt": "2026-06-05T13:40:00Z"
    }
  ],
  "hasMore": true,
  "nextBefore": "msg_000122"
}
```

| Campo                               | Significado                                                                                                                                  |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `direction`                         | `INBOUND` (recebida) ou `OUTBOUND` (enviada por você).                                                                                       |
| `type`                              | `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contact`, `poll` ou `unknown`.                                        |
| `text`                              | Texto/legenda da mensagem (`null` quando não houver).                                                                                        |
| `mediaUrl`                          | URL pré-assinada (validade \~6h), só presente em mensagens de mídia. Releia o endpoint para obter uma nova.                                  |
| `mediaMime`                         | Content-type da mídia (`null` se não for mídia).                                                                                             |
| `status`                            | Estado de entrega da mensagem. Para `OUTBOUND` evolui por `SENT`, `DELIVERED` e `READ`; as recebidas (`INBOUND`) chegam já como `DELIVERED`. |
| `waMessageId` / `quotedWaMessageId` | Id da mensagem no WhatsApp e da mensagem citada (resposta), se houver.                                                                       |

**Paginando o histórico completo:** repita a chamada passando o `nextBefore` da resposta anterior em `?before=` até `hasMore` virar `false`. Quando `hasMore` é `false`, `nextBefore` vem `null` — você chegou ao início da conversa.

Veja o contrato completo, com playground "Try it", em [Lista as conversas](/docs/api/inbox/listInboxConversations) e [Lista as mensagens](/docs/api/inbox/listInboxMessages).

<Callout type="info">
  **Mídia: re-hospedada por nós (por 2 anos).** O binário fica no nosso storage por **2 anos**;
  depois disso é removido (o **texto** da mensagem permanece). As mensagens de mídia trazem uma
  `mediaUrl` **pré-assinada** gerada na hora da leitura (validade \~6h). Se a URL expirar, **basta
  reler o endpoint de mensagens** para obter uma nova — dentro da janela de 2 anos você não precisa
  baixar nem armazenar o arquivo. (A `mediaUrl` que chega no webhook `message.received` é gerada com
  validade de **24h** e serve para uso imediato.) &#x2A;*Se precisar da mídia por mais de 2 anos, baixe e
  armazene do seu lado.**
</Callout>

### Erros possíveis (Inbox) [#erros-possíveis-inbox]

O envelope de erro é `{ "error": { "code", "message" } }`, em que `code` é um **rótulo string** derivado do status HTTP (`UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`).

| Status | `code`         | Significado                                                                                               |
| ------ | -------------- | --------------------------------------------------------------------------------------------------------- |
| `401`  | `UNAUTHORIZED` | API key ausente ou inválida — inclui usar a key no host do modo errado (ex.: `pzk_live_` em `sandbox.*`). |
| `403`  | `FORBIDDEN`    | Projeto inativo ou sem permissão.                                                                         |
| `404`  | `NOT_FOUND`    | Conversa não encontrada (não existe, não é do seu Projeto, ou é de outro modo — teste/produção).          |

## Tempo real, sem polling [#tempo-real-sem-polling]

Para reagir a mensagens novas e a mudanças de status **na hora**, use eventos em vez de ficar consultando a API:

* **Webhook** — entrega garantida com retry (cadastrado por você via `POST /v1/webhooks`, self-service). Ver [Webhooks](/docs/webhooks).
* **WebSocket** — self-service, conecta com a sua API key. Ver [WebSocket](/docs/webhooks/websocket).

O Inbox é a fonte do **histórico**; os eventos são para o **tempo real**. Juntos, cobrem todo o ciclo sem você manter banco próprio. O modo de histórico (persistido/efêmero) **não** altera a entrega desses eventos.

## Migrando de uma stack própria [#migrando-de-uma-stack-própria]

Se você hoje mantém infra própria para o WhatsApp (por exemplo, um banco de mensagens, um bucket de mídia e um canal de realtime), ao integrar o PEPINO ZAP você pode **descontinuar essas três camadas**:

| Sua camada atual                          | Substituto no PEPINO ZAP                                    |
| ----------------------------------------- | ----------------------------------------------------------- |
| Banco de mensagens (ex.: DynamoDB/SQL)    | `GET /v1/inbox/conversations` + `/messages`                 |
| Storage de mídia (ex.: bucket S3)         | Nosso storage (mídia retida 2 anos) + `mediaUrl` na leitura |
| Realtime (ex.: WebSocket/pub-sub próprio) | Webhook `message.received`/`message.status` ou WebSocket    |

<Callout type="warn">
  Guardar uma cópia própria continua sendo **opção sua** (redundância, relatórios offline, retenção
  mais longa) — mas **não é requisito**. Para a maioria das integrações, ler do Inbox quando
  precisar é suficiente.
</Callout>

## Próximos passos [#próximos-passos]

* [Sua primeira instância](/docs/get-started/first-instance) — criar, parear e configurar a instância.
* [Autenticação](/docs/get-started/authentication) — API key, modos teste/produção e o fallback `?token=`.
* [Lista as conversas](/docs/api/inbox/listInboxConversations) — referência completa do Inbox.
* [Webhooks](/docs/webhooks) — receba eventos de mensagens e de status da instância em tempo real.
