diff --git a/docs/VIEW_ONCE.md b/docs/VIEW_ONCE.md
new file mode 100644
index 00000000..c0fdeb33
--- /dev/null
+++ b/docs/VIEW_ONCE.md
@@ -0,0 +1,514 @@
+# Mensagens View-Once (Visualização Única) — Guia de Implementação
+
+**Data:** 2026-03-23
+**Versão:** 1.0
+
+---
+
+## 1. O que é View-Once
+
+Mensagem de visualização única é um tipo especial de mídia (imagem, vídeo ou áudio) que o destinatário pode abrir apenas uma vez. Após abrir, o conteúdo some permanentemente.
+
+**Comportamento em cada dispositivo após envio via API:**
+
+| Dispositivo | O que aparece |
+|---|---|
+| Destinatário (Android/iOS) | Recebe a mídia com ícone de olhinho — pode abrir uma vez |
+| Android principal do remetente | Mostra na conversa que a mensagem foi enviada (ícone de olhinho) |
+| WA Web do remetente | "Aguardando mensagem. Abra o WhatsApp no seu celular." |
+
+> **Nota:** A mensagem "Aguardando mensagem..." no WA Web é o comportamento correto e esperado quando o envio vem de um companion (API). É uma limitação do protocolo WhatsApp — o servidor só permite que o celular principal notifique companions com o conteúdo da view-once.
+
+---
+
+## 2. Como Enviar via API
+
+### 2.1 Imagem
+
+```bash
+curl -X POST https://sua-api.com/v1/messages/send_image \
+ -H "Content-Type: application/json" \
+ -H "x-api-key: SEU_API_KEY" \
+ -d '{
+ "instance": "nome-da-instancia",
+ "to": "5511999999999",
+ "caption": "Olha só isso!",
+ "imageUrl": "https://exemplo.com/foto.jpg",
+ "viewOnce": true
+ }'
+```
+
+**Resposta de sucesso:**
+```json
+{ "ok": true }
+```
+
+### 2.2 Vídeo
+
+```bash
+curl -X POST https://sua-api.com/v1/messages/send_video \
+ -H "Content-Type: application/json" \
+ -H "x-api-key: SEU_API_KEY" \
+ -d '{
+ "instance": "nome-da-instancia",
+ "to": "5511999999999",
+ "caption": "Assista uma vez",
+ "videoUrl": "https://exemplo.com/video.mp4",
+ "viewOnce": true
+ }'
+```
+
+### 2.3 Áudio
+
+```bash
+curl -X POST https://sua-api.com/v1/messages/send_audio \
+ -H "Content-Type: application/json" \
+ -H "x-api-key: SEU_API_KEY" \
+ -d '{
+ "instance": "nome-da-instancia",
+ "to": "5511999999999",
+ "audioUrl": "https://exemplo.com/audio.ogg",
+ "ptt": false,
+ "viewOnce": true
+ }'
+```
+
+### 2.4 Usando Base64 (sem URL pública)
+
+```bash
+curl -X POST https://sua-api.com/v1/messages/send_image \
+ -H "Content-Type: application/json" \
+ -H "x-api-key: SEU_API_KEY" \
+ -d '{
+ "instance": "nome-da-instancia",
+ "to": "5511999999999",
+ "imageBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRgAB...",
+ "viewOnce": true
+ }'
+```
+
+---
+
+## 3. Como Receber via Webhook
+
+Quando um contato envia uma mensagem de visualização única para o número conectado na API, o webhook recebe o evento com `key.isViewOnce = true`.
+
+### 3.1 Estrutura do evento recebido
+
+```json
+{
+ "event": "messages.upsert",
+ "instance": "nome-da-instancia",
+ "data": {
+ "messages": [
+ {
+ "key": {
+ "remoteJid": "5511999999999@s.whatsapp.net",
+ "fromMe": false,
+ "id": "3EB0ABCDEF123456",
+ "isViewOnce": true
+ },
+ "message": {
+ "viewOnceMessageV2": {
+ "message": {
+ "imageMessage": {
+ "url": "https://mmg.whatsapp.net/...",
+ "mimetype": "image/jpeg",
+ "mediaKey": "base64...",
+ "fileEncSha256": "base64...",
+ "directPath": "/v/...",
+ "viewOnce": true
+ }
+ }
+ }
+ },
+ "messageTimestamp": 1742700000,
+ "pushName": "João Silva"
+ }
+ ],
+ "type": "notify"
+ }
+}
+```
+
+### 3.2 Como identificar que é view-once
+
+O campo principal para identificar é `key.isViewOnce = true`. Existem dois caminhos para confirmar:
+
+```javascript
+function isViewOnce(msg) {
+ // Caminho 1: flag direta (mais simples)
+ if (msg.key?.isViewOnce) return true
+
+ // Caminho 2: verificar o wrapper da mensagem
+ const inner =
+ msg.message?.viewOnceMessageV2?.message ||
+ msg.message?.viewOnceMessage?.message ||
+ msg.message?.viewOnceMessageV2Extension?.message
+
+ return !!(
+ inner?.imageMessage?.viewOnce ||
+ inner?.videoMessage?.viewOnce ||
+ inner?.audioMessage?.viewOnce
+ )
+}
+```
+
+### 3.3 Como acessar a mídia
+
+```javascript
+function getViewOnceMedia(msg) {
+ const inner =
+ msg.message?.viewOnceMessageV2?.message ||
+ msg.message?.viewOnceMessage?.message ||
+ msg.message?.viewOnceMessageV2Extension?.message
+
+ if (!inner) return null
+
+ if (inner.imageMessage) return { type: 'image', media: inner.imageMessage }
+ if (inner.videoMessage) return { type: 'video', media: inner.videoMessage }
+ if (inner.audioMessage) return { type: 'audio', media: inner.audioMessage }
+
+ return null
+}
+
+// Uso:
+const media = getViewOnceMedia(msg)
+if (media) {
+ console.log(`View-once ${media.type}`)
+ console.log('URL:', media.media.url)
+ console.log('DirectPath:', media.media.directPath)
+ console.log('MediaKey:', media.media.mediaKey) // necessário para download
+}
+```
+
+---
+
+## 4. Como Exibir numa Ferramenta (Frontend/CRM)
+
+### 4.1 Lógica de detecção e exibição
+
+```javascript
+function renderMessage(msg) {
+ if (!isViewOnce(msg)) {
+ // Renderizar mensagem normal
+ return renderNormalMessage(msg)
+ }
+
+ const media = getViewOnceMedia(msg)
+ if (!media) return
+
+ // Verificar se já foi aberta (controle local da ferramenta)
+ const alreadyOpened = localStorage.getItem(`vo_${msg.key.id}`)
+
+ if (alreadyOpened) {
+ // Mostrar placeholder de "já visualizado"
+ return renderViewOncePlaceholder(media.type, msg.key.fromMe)
+ }
+
+ if (msg.key.fromMe) {
+ // Mensagem enviada por mim — nunca mostrar conteúdo
+ return renderSentViewOnce(media.type)
+ }
+
+ // Mensagem recebida — mostrar botão para "abrir uma vez"
+ return renderViewOnceButton(media.type, msg)
+}
+```
+
+### 4.2 Componente de exibição — mensagem recebida
+
+```javascript
+function renderViewOnceButton(type, msg) {
+ const icons = { image: '📷', video: '🎥', audio: '🎵' }
+ const labels = { image: 'Foto', video: 'Vídeo', áudio: 'Áudio' }
+
+ return `
+
+ ${icons[type]}
+ ${labels[type]} de visualização única
+
+
+ `
+}
+
+async function openViewOnce(msgId, type) {
+ // 1. Marcar como aberta ANTES de mostrar (só uma chance)
+ localStorage.setItem(`vo_${msgId}`, '1')
+
+ // 2. Baixar e mostrar a mídia
+ const mediaUrl = await downloadViewOnceMedia(msgId)
+ showMedia(type, mediaUrl)
+
+ // 3. Após fechar/visualizar, substituir pelo placeholder
+ onMediaClosed(() => {
+ replaceWithPlaceholder(msgId, type)
+ })
+}
+```
+
+### 4.3 Componente de exibição — mensagem enviada
+
+```javascript
+function renderSentViewOnce(type) {
+ const labels = { image: 'Foto', video: 'Vídeo', audio: 'Áudio' }
+ return `
+
+ 👁️
+ ${labels[type]} de visualização única
+ Enviada
+
+ `
+}
+```
+
+### 4.4 Placeholder após visualização
+
+```javascript
+function renderViewOncePlaceholder(type, fromMe) {
+ const labels = { image: 'foto', video: 'vídeo', audio: 'áudio' }
+ const text = fromMe
+ ? `Você enviou uma ${labels[type]} de visualização única`
+ : `${labels[type]} de visualização única aberta`
+
+ return `
+
+ 🚫
+ ${text}
+
+ `
+}
+```
+
+### 4.5 CSS sugerido
+
+```css
+.view-once-bubble {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ border-radius: 12px;
+ max-width: 280px;
+}
+
+.view-once-bubble.received {
+ background: #f0f0f0;
+ color: #333;
+}
+
+.view-once-bubble.sent {
+ background: #dcf8c6;
+ color: #333;
+ align-self: flex-end;
+}
+
+.view-once-bubble.placeholder {
+ background: #e8e8e8;
+ color: #999;
+ font-style: italic;
+}
+
+.view-once-bubble button {
+ background: #25d366;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ padding: 6px 12px;
+ cursor: pointer;
+ font-size: 13px;
+}
+```
+
+---
+
+## 5. Código Implementado na API
+
+### 5.1 Geração da mensagem — `src/Utils/messages.ts`
+
+Quando `viewOnce: true` é passado, a mídia é encapsulada no wrapper moderno `viewOnceMessageV2` (campo 55 do proto) e o flag `viewOnce=true` é setado na mídia interna:
+
+```typescript
+if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
+ // Seta viewOnce=true na mensagem de mídia interna
+ if (m.imageMessage) m.imageMessage.viewOnce = true
+ else if (m.videoMessage) m.videoMessage.viewOnce = true
+ else if (m.audioMessage) m.audioMessage.viewOnce = true
+ // Usa viewOnceMessageV2 (campo 55) — formato atual do WA
+ m = { viewOnceMessageV2: { message: m } }
+}
+```
+
+### 5.2 Envio — `src/Socket/messages-send.ts`
+
+**Detecção de view-once real** (não confunde com botões/listas que também usam o wrapper `viewOnceMessage`):
+
+```typescript
+// Detecta view-once real pela flag viewOnce na mídia interna.
+// viewOnceMessage* wrappers são também usados por botões/listas/carrosséis
+// (que carregam interactiveMessage dentro) — esses NÃO têm viewOnce=true na mídia.
+const viewOnceInner =
+ message.viewOnceMessageV2?.message ||
+ message.viewOnceMessage?.message ||
+ message.viewOnceMessageV2Extension?.message
+const isViewOnceMsg = !!(
+ viewOnceInner?.imageMessage?.viewOnce ||
+ viewOnceInner?.videoMessage?.viewOnce ||
+ viewOnceInner?.audioMessage?.viewOnce
+)
+
+// Para view-once: DSM enviado apenas para o celular principal (device=0).
+// Companions (device>0) são omitidos — o servidor WA gera
+// automaticamente para eles.
+const viewOnceMeRecipients = isViewOnceMsg
+ ? meRecipients.filter(jid => !jidDecode(jid)?.device)
+ : meRecipients
+
+// assertSessions apenas para quem vai realmente receber
+await assertSessions([...viewOnceMeRecipients, ...otherRecipients])
+
+const [meNodes, otherNodes] = await Promise.all([
+ createParticipantNodes(viewOnceMeRecipients, meMsg || message, extraAttrs),
+ createParticipantNodes(otherRecipients, message, extraAttrs)
+])
+participants.push(...meNodes)
+participants.push(...otherNodes)
+
+// phash recalculado com os participantes reais
+const phashRecipients = isViewOnceMsg
+ ? [...viewOnceMeRecipients, ...otherRecipients]
+ : [...meRecipients, ...otherRecipients]
+if (phashRecipients.length > 0) {
+ extraAttrs['phash'] = generateParticipantHashV2(phashRecipients)
+}
+```
+
+**`getMediaType()` — garante que vídeo e áudio view-once sejam entregues:**
+
+```typescript
+const getMediaType = (message: proto.IMessage) => {
+ // Desencapsula wrapper view-once antes de verificar o tipo de mídia.
+ // Sem isso, message.videoMessage e message.audioMessage são undefined
+ // (estão dentro do wrapper) e o atributo mediatype ficaria ausente no
+ // enc node — o servidor WA descarta silenciosamente vídeo/áudio view-once.
+ const inner =
+ message.viewOnceMessage?.message ||
+ message.viewOnceMessageV2?.message ||
+ message.viewOnceMessageV2Extension?.message
+ if (inner) {
+ return getMediaType(inner)
+ }
+
+ if (message.imageMessage) return 'image'
+ else if (message.videoMessage) return 'video'
+ // ...
+}
+```
+
+### 5.3 Recepção — `src/Utils/decode-wa-message.ts`
+
+Quando a API recebe uma view-once como companion (linked device), o servidor envia dois stanzas:
+
+- **Stanza 1** (`enc`): conteúdo completo com metadados da mídia
+- **Stanza 2** (`unavailable type="view_once"`): sinal de sync
+
+O stanza 1 é detectado e marcado corretamente:
+
+```typescript
+// Detecta view-once na stanza 1 recebida por linked device.
+// O wrapper viewOnceMessage também é usado por mensagens interativas
+// (interactiveMessage, listMessage) — esses NÃO têm viewOnce=true na mídia.
+const viewOnceInner =
+ msg.viewOnceMessage?.message ||
+ msg.viewOnceMessageV2?.message ||
+ msg.viewOnceMessageV2Extension?.message
+if (
+ viewOnceInner?.imageMessage?.viewOnce ||
+ viewOnceInner?.videoMessage?.viewOnce ||
+ viewOnceInner?.audioMessage?.viewOnce
+) {
+ fullMessage.key.isViewOnce = true
+}
+```
+
+---
+
+## 6. Fluxo Completo
+
+```
+API envia view-once para destinatário
+│
+├── Protobuf gerado:
+│ viewOnceMessageV2 {
+│ message {
+│ imageMessage { viewOnce: true, url, mediaKey, ... }
+│ }
+│ }
+│
+├── Stanza enviado ao servidor WA:
+│
+│ ...viewOnceMessageV2 cifrado... ← para o destinatário
+│
+│ ← celular principal
+│ ...DSM{viewOnceMessageV2}...
+│
+│
+│
+│
+│
+└── Distribuição pelo servidor:
+ ├── Destinatário → recebe enc → abre uma vez ✅
+ ├── Celular principal → recebe DSM → mostra "você enviou" ✅
+ └── WA Web → recebe → "Aguardando..." ✅
+
+
+Destinatário envia view-once de volta para a API
+│
+├── Stanza recebido pela API (stanza 1 — enc com conteúdo):
+│
+│ ...viewOnceMessageV2 cifrado...
+│
+│
+├── decode-wa-message.ts:
+│ └── Descriptografa → detecta viewOnce=true → seta key.isViewOnce=true
+│
+├── Stanza recebido (stanza 2 — unavailable, ignorado):
+│
+│
+│
+│
+└── Webhook emitido:
+ messages.upsert → msg.key.isViewOnce = true ✅
+```
+
+---
+
+## 7. Limitações
+
+| Situação | Comportamento | Motivo |
+|---|---|---|
+| WA Web do remetente | "Aguardando mensagem. Abra no celular." | Servidor WA só aceita `` vindo do celular principal, não de companions |
+| View-once de documento/sticker | Não suportado | Limitação do protocolo WA — só imagem, vídeo e áudio |
+| `error_479` nos logs | Normal, não é erro | TcToken é atualizado após o envio de view-once |
+| Companion não consegue abrir | Por design | O conteúdo não é entregue para linked devices |
+
+---
+
+## 8. Checklist de Validação
+
+**Envio:**
+- [ ] Destinatário recebe com ícone de olhinho
+- [ ] Destinatário consegue abrir a mídia
+- [ ] Após abrir, a mídia some
+- [ ] Celular principal do remetente mostra "você enviou" com ícone de olhinho
+- [ ] WA Web do remetente mostra "Aguardando mensagem..."
+- [ ] Funciona para imagem, vídeo e áudio
+- [ ] Logs mostram `📤 Message sent` (error_479 é normal)
+
+**Recepção:**
+- [ ] Webhook recebe `key.isViewOnce = true`
+- [ ] `viewOnceMessageV2.message.imageMessage` (ou video/audio) está presente
+- [ ] API não crasha ao receber view-once de outro dispositivo
+- [ ] Mensagem interativa (botão/lista) enviada após view-once não é afetada