# Tipos de Disparo — baileys_interactive

> Documentação dos 6 tipos de mensagem disponíveis no `<select id="dispatchType">`.  
> Cada seção lista os arquivos envolvidos e os trechos de código **comentados** linha a linha.

---

## Mapa de Arquivos

| Camada | Arquivo | Papel |
|--------|---------|-------|
| **Frontend — HTML** | `public/index.html` | Formulários de cada tipo de disparo (linhas 82–159) |
| **Frontend — JS** | `public/app.js` | Switch de formulários, builders de payload, loop de envio |
| **Backend — Rotas** | `src/routes/messages.ts` | Endpoints POST de cada tipo (`/v1/messages/send_*`) |
| **Backend — Tipos** | `src/types/whatsapp.ts` | Interfaces TypeScript para os payloads do socket |
| **Backend — Config** | `src/config.ts` | Limites de botões, cards, opções de poll etc. |
| **Backend — Helpers** | `src/utils/helpers.ts` | `toJid()` (formata número → JID) e `isConnected()` |
| **Backend — Socket** | `src/services/whatsapp.ts` | Gerencia instâncias Baileys (criar, conectar, desconectar) |

---

## Como o Fluxo Funciona (visão geral)

```
[Usuário seleciona tipo no <select>]
        ↓
app.js: evento 'change' esconde todos os <div id="formXxx">
        e exibe apenas o div do tipo escolhido
        ↓
[Usuário preenche campos e clica "Enviar"]
        ↓
app.js: getXxxPayload() monta { url, body }
        ↓
app.js: loop por destinatários → fetch POST para o endpoint
        ↓
messages.ts: valida instância + conectividade
        ↓
whatsapp.ts: ctx.sock.sendMessage(jid, payload_baileys)
        ↓
[Mensagem entregue via InfiniteAPI/Baileys]
```

---

## 1. Menu (texto numerado)

**Valor no select:** `menu`  
**Endpoint:** `POST /v1/messages/send_menu`  
**Descrição:** Monta uma mensagem de texto simples com opções numeradas em negrito. O usuário responde digitando o número.

### Frontend — `public/index.html` (linhas 93–103)

```html
<!-- Formulário visível quando tipo = "menu" -->
<div id="formMenu" class="dispatch-form">
  <!-- Título opcional em negrito no topo da mensagem -->
  <div class="form-row">
    <label>Título</label>
    <input type="text" id="menuTitle" placeholder="Menu de Opções">
  </div>

  <!-- Texto introdutório antes das opções -->
  <div class="form-row">
    <label>Texto</label>
    <textarea id="menuText" rows="2" placeholder="Escolha uma opção:"></textarea>
  </div>

  <!-- Lista dinâmica de opções — cada item vira uma linha "1. Opção" -->
  <div class="form-row">
    <label>Opções</label>
    <div id="menuOptionsList" class="dynamic-list"></div>
    <!-- data-for="menuOptions" → app.js chama addMenuOption() ao clicar -->
    <button type="button" class="btn btn-small btn-ghost add-item" data-for="menuOptions">
      + Adicionar opção
    </button>
  </div>

  <!-- Rodapé em itálico no final da mensagem -->
  <div class="form-row">
    <label>Rodapé</label>
    <input type="text" id="menuFooter" placeholder="Responda com número">
  </div>
</div>
```

### Frontend — `public/app.js`

```js
// --- Adiciona uma linha de input para nova opção (chamado via data-for="menuOptions") ---
function addMenuOption() {
  // addRow(containerId, htmlInterna):
  //   cria um div.item-row com o input + botão "Remover" automático
  addRow('menuOptionsList', '<input type="text" placeholder="Texto da opção" data-field="opt">');
}

// --- Monta o payload para envio ao backend ---
function getMenuPayload() {
  const options = [];

  // Coleta o valor de cada input data-field="opt" não vazio
  document.querySelectorAll('#menuOptionsList .item-row input[data-field="opt"]').forEach((inp) => {
    const v = inp.value.trim();
    if (v) options.push(v);
  });

  return {
    url: '/v1/messages/send_menu',   // endpoint do backend
    body: {
      instance: document.getElementById('dispatchInstance').value,  // instância WhatsApp
      to: document.getElementById('dispatchTo').value.trim(),        // número(s) destino
      title: document.getElementById('menuTitle').value.trim() || 'Menu',
      text: document.getElementById('menuText').value.trim() || 'Escolha uma opção:',
      options: options.length ? options : ['Opção 1'],               // fallback se vazio
      footer: document.getElementById('menuFooter').value.trim() || undefined,
    },
  };
}
```

### Backend — `src/routes/messages.ts` (linhas 22–58)

```ts
// POST /v1/messages/send_menu
router.post('/send_menu', async (req: Request, res: Response) => {
  try {
    // Desestrutura o body: instance (default 'main'), número destino, título, texto, opções e rodapé
    const { instance = 'main', to, title, text, options, footer } = req.body;

    // Valida presença obrigatória: destino e ao menos 1 opção
    if (!to || !Array.isArray(options) || options.length === 0) {
      return res.status(400).json({ ok: false, error: 'missing to/options' });
    }

    // Busca e valida a instância Baileys (deve existir e estar conectada)
    const ctx = validateInstance(instance, res);
    if (!ctx) return;

    // Converte número para JID WhatsApp: "5511999..." → "5511999...@s.whatsapp.net"
    const jid = toJid(to);
    if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' });

    // Monta o texto numerado:
    // *Título*\n\nTexto\n\n*1.* Opção 1\n*2.* Opção 2\n\n_Rodapé_
    let menuText = '';
    if (title) menuText += `*${title}*\n\n`;            // título em negrito
    if (text)  menuText += `${text}\n\n`;               // texto introdutório
    options.forEach((opt, idx) => {
      // Suporta opção como string simples ou objeto { text }
      const label = typeof opt === 'string' ? opt : opt.text ?? `Opção ${idx + 1}`;
      menuText += `*${idx + 1}.* ${label}\n`;           // "1. Opção" em negrito
    });
    if (footer) menuText += `\n_${footer}_`;            // rodapé em itálico

    // Envia mensagem de texto simples (sem recurso especial do Baileys)
    await ctx.sock.sendMessage(jid, { text: menuText.trim() });

    return res.json({ ok: true, hint: 'User should reply with the option number (1, 2, 3...)' });
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return res.status(500).json({ ok: false, error: message });
  }
});
```

**Payload de exemplo (JSON enviado pelo frontend):**
```json
{
  "instance": "main",
  "to": "5511999999999",
  "title": "Menu de Opções",
  "text": "Escolha uma opção:",
  "options": ["Produto A", "Produto B", "Falar com atendente"],
  "footer": "Responda com o número"
}
```

---

## 2. Botões quick reply

**Valor no select:** `buttons`  
**Endpoint:** `POST /v1/messages/send_buttons_helpers`  
**Descrição:** Mensagem com até **3 botões clicáveis** de resposta rápida (`nativeButtons` do Baileys). O usuário toca no botão sem digitar nada.

### Frontend — `public/index.html` (linhas 105–114)

```html
<!-- Formulário visível quando tipo = "buttons" -->
<div id="formButtons" class="dispatch-form hidden">
  <!-- Corpo da mensagem -->
  <div class="form-row">
    <label>Texto</label>
    <textarea id="buttonsText" rows="2" placeholder="Como posso ajudar?"></textarea>
  </div>

  <!-- Texto secundário abaixo da mensagem principal -->
  <div class="form-row">
    <label>Rodapé</label>
    <input type="text" id="buttonsFooter" placeholder="Atendimento 24h">
  </div>

  <!-- Até 3 botões: cada linha tem ID (identificador interno) + Texto visível -->
  <div class="form-row">
    <label>Botões (máx 3)</label>
    <div id="buttonsList" class="dynamic-list"></div>
    <button type="button" class="btn btn-small btn-ghost add-item" data-for="buttons">
      + Adicionar botão
    </button>
  </div>
</div>
```

### Frontend — `public/app.js`

```js
// Adiciona linha com 2 inputs: ID do botão e texto visível
function addButtonRow() {
  addRow(
    'buttonsList',
    '<input type="text" placeholder="ID do botão" data-field="id">' +
    '<input type="text" placeholder="Texto do botão" data-field="text">'
  );
}

function getButtonsPayload() {
  const buttons = [];

  // Coleta pares (id, text) de cada linha — ignora linhas incompletas
  document.querySelectorAll('#buttonsList .item-row').forEach((row) => {
    const id   = row.querySelector('[data-field="id"]')?.value?.trim();
    const text = row.querySelector('[data-field="text"]')?.value?.trim();
    if (id && text) buttons.push({ id, text });
  });

  return {
    url: '/v1/messages/send_buttons_helpers',
    body: {
      instance: document.getElementById('dispatchInstance').value,
      to: document.getElementById('dispatchTo').value.trim(),
      text: document.getElementById('buttonsText').value.trim() || 'Escolha:',
      footer: document.getElementById('buttonsFooter').value.trim() || undefined,
      // Limita a 3 botões no frontend; o backend também aplica o limit de config
      buttons: buttons.length ? buttons.slice(0, 3) : [{ id: 'btn1', text: 'Opção 1' }],
    },
  };
}
```

### Backend — `src/routes/messages.ts` (linhas 61–99)

```ts
// POST /v1/messages/send_buttons_helpers
router.post('/send_buttons_helpers', async (req: Request, res: Response) => {
  try {
    const { instance = 'main', to, text, buttons, footer } = req.body;

    if (!to || !text || !Array.isArray(buttons) || buttons.length === 0) {
      return res.status(400).json({ ok: false, error: 'missing to/text/buttons' });
    }

    // config.limits.maxButtons = 3 (definido em src/config.ts)
    const limited = buttons.slice(0, config.limits.maxButtons);

    const ctx = validateInstance(instance, res);
    if (!ctx) return;

    const jid = toJid(to);
    if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' });

    // Converte array de { id, text } para o formato nativeButtons do InfiniteAPI/Baileys
    const nativeButtons = limited.map((btn, idx) => ({
      type: 'reply' as const,       // tipo "reply" = botão de resposta rápida
      id: btn.id ?? `btn_${idx}`,   // ID retornado quando o usuário clicar
      text: btn.text ?? `Botão ${idx + 1}`,
    }));

    // sendMessage com nativeButtons ativa o recurso de quick reply no WhatsApp
    const result = await ctx.sock.sendMessage(jid, {
      nativeButtons,
      text: String(text),
      footer: footer ? String(footer) : undefined,
    });

    return res.json({ ok: true, format: 'nativeButtons', messageId: result?.key?.id });
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return res.status(500).json({ ok: false, error: message });
  }
});
```

**Payload de exemplo:**
```json
{
  "instance": "main",
  "to": "5511999999999",
  "text": "Como posso ajudar?",
  "footer": "Atendimento 24h",
  "buttons": [
    { "id": "btn_sim",    "text": "✅ Sim" },
    { "id": "btn_nao",    "text": "❌ Não" },
    { "id": "btn_ajuda",  "text": "🆘 Ajuda" }
  ]
}
```

---

## 3. Botões CTA (URL / Copiar / Ligar)

**Valor no select:** `interactive`  
**Endpoint:** `POST /v1/messages/send_interactive_helpers`  
**Descrição:** Botões de ação que abrem URLs, copiam código ou ligam para um número. Usa também `nativeButtons` mas com tipos `url`, `copy` e `call`.

### Frontend — `public/index.html` (linhas 116–125)

```html
<!-- Formulário visível quando tipo = "interactive" -->
<div id="formInteractive" class="dispatch-form hidden">
  <div class="form-row">
    <label>Texto</label>
    <textarea id="interactiveText" rows="3" placeholder="Mensagem com CTAs"></textarea>
  </div>
  <div class="form-row">
    <label>Rodapé</label>
    <input type="text" id="interactiveFooter">
  </div>

  <!-- Cada botão CTA tem: tipo (url/copy/call) + texto visível + valor extra -->
  <div class="form-row">
    <label>Botões CTA</label>
    <div id="interactiveList" class="dynamic-list"></div>
    <button type="button" class="btn btn-small btn-ghost add-item" data-for="interactive">
      + Adicionar botão
    </button>
  </div>
</div>
```

### Frontend — `public/app.js`

```js
// Cada linha CTA tem: select de tipo + texto + valor extra (url/código/fone)
function addInteractiveRow() {
  addRow(
    'interactiveList',
    `<select data-field="type">
       <option value="url">URL</option>
       <option value="copy">Copiar</option>
       <option value="call">Ligar</option>
     </select>
     <input type="text" placeholder="Texto do botão" data-field="text">
     <input type="text" placeholder="URL / Código / Telefone" data-field="extra">`
  );
}

function getInteractivePayload() {
  const buttons = [];

  document.querySelectorAll('#interactiveList .item-row').forEach((row) => {
    const type  = row.querySelector('[data-field="type"]')?.value || 'url';
    const text  = row.querySelector('[data-field="text"]')?.value?.trim();
    const extra = row.querySelector('[data-field="extra"]')?.value?.trim();
    if (!text || !extra) return; // ignora linhas incompletas

    const btn = { type, text };
    // O campo "extra" tem significado diferente conforme o tipo:
    if (type === 'url')  btn.url         = extra; // link a abrir
    if (type === 'copy') btn.copyCode    = extra; // código a copiar (ex: cupom)
    if (type === 'call') btn.phoneNumber = extra; // número a ligar
    buttons.push(btn);
  });

  return {
    url: '/v1/messages/send_interactive_helpers',
    body: {
      instance: document.getElementById('dispatchInstance').value,
      to: document.getElementById('dispatchTo').value.trim(),
      text: document.getElementById('interactiveText').value.trim() || 'Confira:',
      footer: document.getElementById('interactiveFooter').value.trim() || undefined,
      buttons,
    },
  };
}
```

### Backend — `src/routes/messages.ts` (linhas 101–154)

```ts
// POST /v1/messages/send_interactive_helpers
router.post('/send_interactive_helpers', async (req: Request, res: Response) => {
  try {
    const { instance = 'main', to, text, buttons, footer } = req.body;

    if (!to || !text || !Array.isArray(buttons) || buttons.length === 0) {
      return res.status(400).json({ ok: false, error: 'missing to/text/buttons' });
    }

    const ctx = validateInstance(instance, res);
    if (!ctx) return;

    const jid = toJid(to);
    if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' });

    // Converte cada botão para o formato nativo do InfiniteAPI:
    const nativeButtons = buttons.slice(0, config.limits.maxButtons).map((btn, idx) => {
      const type = (btn.type ?? 'reply').toLowerCase();

      if (type === 'url' || btn.url) {
        // Botão que abre uma URL no navegador
        return { type: 'url' as const, text: btn.text ?? 'Abrir', url: btn.url! };
      }
      if (type === 'copy' || btn.copyCode || btn.copyText) {
        // Botão que copia um código/texto para a área de transferência
        return { type: 'copy' as const, text: btn.text ?? 'Copiar', copyText: btn.copyCode ?? btn.copyText ?? '' };
      }
      if (type === 'call' || btn.phoneNumber) {
        // Botão que inicia uma chamada telefônica
        return { type: 'call' as const, text: btn.text ?? 'Ligar', phoneNumber: btn.phoneNumber! };
      }
      // Fallback: reply simples
      return { type: 'reply' as const, id: `btn_${idx}`, text: btn.text ?? 'Botão' };
    });

    const result = await ctx.sock.sendMessage(jid, {
      nativeButtons,
      text: String(text),
      footer: footer ? String(footer) : undefined,
    });

    return res.json({ ok: true, format: 'nativeButtons', messageId: result?.key?.id });
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return res.status(500).json({ ok: false, error: message });
  }
});
```

**Payload de exemplo:**
```json
{
  "instance": "main",
  "to": "5511999999999",
  "text": "Confira nossas opções:",
  "buttons": [
    { "type": "url",  "text": "🌐 Ver site",    "url": "https://exemplo.com" },
    { "type": "copy", "text": "📋 Copiar cupom","copyCode": "DESC10" },
    { "type": "call", "text": "📞 Ligar",       "phoneNumber": "5511999999999" }
  ]
}
```

---

## 4. Lista dropdown

**Valor no select:** `list`  
**Endpoint:** `POST /v1/messages/send_list_helpers`  
**Descrição:** Mensagem com botão que abre uma lista em dropdown. Suporta seções e itens com ID, título e descrição. Usa `nativeList` do InfiniteAPI.

### Frontend — `public/index.html` (linhas 127–137)

```html
<!-- Formulário visível quando tipo = "list" -->
<div id="formList" class="dispatch-form hidden">
  <div class="form-row">
    <label>Texto</label>
    <textarea id="listText" rows="2" placeholder="Escolha uma categoria:"></textarea>
  </div>

  <!-- Texto que aparece no botão que abre a lista -->
  <div class="form-row">
    <label>Texto do botão</label>
    <input type="text" id="listButtonText" value="Ver Cardápio" placeholder="Ver opções">
  </div>

  <div class="form-row">
    <label>Rodapé</label>
    <input type="text" id="listFooter" placeholder="Delivery grátis acima de R$ 50">
  </div>

  <!-- Seções: cada seção tem um título e N itens (id + título + descrição) -->
  <div class="form-row">
    <label>Seções</label>
    <div id="listSectionsList" class="dynamic-list"></div>
    <button type="button" class="btn btn-small btn-ghost add-item" data-for="listSections">
      + Adicionar seção
    </button>
  </div>
</div>
```

### Frontend — `public/app.js`

```js
// Cria um bloco "seção" com título + lista de itens aninhados
function addListSection() {
  const container = document.getElementById('listSectionsList');
  const block = document.createElement('div');
  block.className = 'block-section'; // classe usada pelo backend para buscar seções
  block.innerHTML = `
    <div class="block-title">Seção</div>
    <input type="text" class="section-title" placeholder="Título da seção">
    <div class="sub-list section-rows"></div>
    <button type="button" class="btn btn-small btn-ghost add-row-in-section">+ Adicionar item</button>
    <button type="button" class="btn btn-small btn-danger btn-remove-block">Remover seção</button>
  `;

  // Botão "Adicionar item" dentro da seção cria um item com id / título / descrição
  block.querySelector('.add-row-in-section').addEventListener('click', () => {
    const row = document.createElement('div');
    row.className = 'item-row';
    row.innerHTML = `
      <input type="text" placeholder="ID"        data-field="id">
      <input type="text" placeholder="Título"    data-field="title">
      <input type="text" placeholder="Descrição" data-field="desc">
      <button type="button" class="btn btn-small btn-ghost btn-remove">Remover</button>
    `;
    row.querySelector('.btn-remove').onclick = () => row.remove();
    block.querySelector('.section-rows').appendChild(row);
  });

  block.querySelector('.btn-remove-block').onclick = () => block.remove();
  container.appendChild(block);
}

function getListPayload() {
  const sections = [];

  // Itera cada bloco-seção e coleta título + seus itens
  document.querySelectorAll('#listSectionsList .block-section').forEach((block) => {
    const title = block.querySelector('.section-title')?.value?.trim() || 'Seção';
    const rows = [];
    block.querySelectorAll('.section-rows .item-row').forEach((row) => {
      const id    = row.querySelector('[data-field="id"]')?.value?.trim();
      const titleR= row.querySelector('[data-field="title"]')?.value?.trim();
      const desc  = row.querySelector('[data-field="desc"]')?.value?.trim();
      if (id && titleR) rows.push({ id, title: titleR, description: desc || '' });
    });
    if (rows.length) sections.push({ title, rows });
  });

  return {
    url: '/v1/messages/send_list_helpers',
    body: {
      instance: document.getElementById('dispatchInstance').value,
      to: document.getElementById('dispatchTo').value.trim(),
      text: document.getElementById('listText').value.trim() || 'Escolha:',
      buttonText: document.getElementById('listButtonText').value.trim() || 'Ver opções',
      footer: document.getElementById('listFooter').value.trim() || undefined,
      // Fallback: 1 seção com 1 item se o usuário não preencheu nada
      sections: sections.length ? sections : [
        { title: 'Opções', rows: [{ id: 'opt1', title: 'Opção 1', description: '' }] }
      ],
    },
  };
}
```

### Backend — `src/routes/messages.ts` (linhas 156–199)

```ts
// POST /v1/messages/send_list_helpers
router.post('/send_list_helpers', async (req: Request, res: Response) => {
  try {
    const { instance = 'main', to, text, footer, buttonText, sections } = req.body;

    if (!to || !text || !buttonText || !Array.isArray(sections) || sections.length === 0) {
      return res.status(400).json({ ok: false, error: 'missing to/text/buttonText/sections' });
    }

    const ctx = validateInstance(instance, res);
    if (!ctx) return;

    const jid = toJid(to);
    if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' });

    // nativeList é o formato do InfiniteAPI para listas dropdown
    const result = await ctx.sock.sendMessage(jid, {
      nativeList: {
        buttonText: String(buttonText), // texto do botão que abre o dropdown
        sections: sections.map((s) => ({
          title: s.title ?? 'Opções',
          rows: (s.rows ?? []).map((row, idx) => ({
            id:          row.id          ?? `row_${idx}`,  // identificador retornado ao clicar
            title:       row.title       ?? 'Item',
            description: row.description ?? '',             // subtítulo opcional
          })),
        })),
      },
      text:   String(text),
      footer: footer ? String(footer) : undefined,
    });

    return res.json({ ok: true, format: 'nativeList', messageId: result?.key?.id });
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return res.status(500).json({ ok: false, error: message });
  }
});
```

**Payload de exemplo:**
```json
{
  "instance": "main",
  "to": "5511999999999",
  "text": "Escolha uma categoria:",
  "buttonText": "Ver Cardápio",
  "footer": "Delivery grátis acima de R$ 50",
  "sections": [
    {
      "title": "🍕 Pizzas",
      "rows": [
        { "id": "pizza_marg", "title": "Margherita",  "description": "Molho, mozzarella, manjericão" },
        { "id": "pizza_cal",  "title": "Calabresa",   "description": "Molho, calabresa, cebola" }
      ]
    },
    {
      "title": "🥤 Bebidas",
      "rows": [
        { "id": "beb_refri", "title": "Refrigerante", "description": "Lata 350ml" }
      ]
    }
  ]
}
```

---

## 5. Enquete (Poll)

**Valor no select:** `poll`  
**Endpoint:** `POST /v1/messages/send_poll`  
**Descrição:** Cria uma enquete nativa do WhatsApp. O usuário vota sem precisar digitar. Suporta múltipla escolha via `selectableCount`.

### Frontend — `public/index.html` (linhas 139–148)

```html
<!-- Formulário visível quando tipo = "poll" -->
<div id="formPoll" class="dispatch-form hidden">
  <!-- Pergunta da enquete (título exibido pelo WhatsApp) -->
  <div class="form-row">
    <label>Pergunta</label>
    <input type="text" id="pollName" placeholder="Qual sua linguagem favorita?">
  </div>

  <!-- Quantas opções o usuário pode selecionar (1 = escolha única) -->
  <div class="form-row">
    <label>Quantas pode escolher</label>
    <input type="number" id="pollSelectable" value="1" min="1">
  </div>

  <!-- Lista de opções de resposta (mín. 2, máx. 12 — limite do WhatsApp) -->
  <div class="form-row">
    <label>Opções</label>
    <div id="pollOptionsList" class="dynamic-list"></div>
    <button type="button" class="btn btn-small btn-ghost add-item" data-for="pollOptions">
      + Adicionar opção
    </button>
  </div>
</div>
```

### Frontend — `public/app.js`

```js
// Cada opção de poll é um input de texto simples
function addPollOption() {
  addRow('pollOptionsList', '<input type="text" placeholder="Opção" data-field="opt">');
}

function getPollPayload() {
  const options = [];

  document.querySelectorAll('#pollOptionsList .item-row input[data-field="opt"]').forEach((inp) => {
    const v = inp.value.trim();
    if (v) options.push(v);
  });

  return {
    url: '/v1/messages/send_poll',
    body: {
      instance: document.getElementById('dispatchInstance').value,
      to: document.getElementById('dispatchTo').value.trim(),
      name: document.getElementById('pollName').value.trim() || 'Enquete',
      // Mínimo 2 opções; fallback se o usuário não preencheu
      options: options.length >= 2 ? options : ['Sim', 'Não'],
      selectableCount: parseInt(document.getElementById('pollSelectable').value, 10) || 1,
    },
  };
}
```

### Backend — `src/routes/messages.ts` (linhas 201–236)

```ts
// POST /v1/messages/send_poll
router.post('/send_poll', async (req: Request, res: Response) => {
  try {
    const { instance = 'main', to, name, options, selectableCount } = req.body;

    // Enquete exige ao menos 2 opções
    if (!to || !name || !Array.isArray(options) || options.length < 2) {
      return res.status(400).json({ ok: false, error: 'missing to/name/options (min 2)' });
    }

    // Limita a 12 opções (config.limits.maxPollOptions) e garante strings
    const opts = options.slice(0, config.limits.maxPollOptions).map((o) =>
      typeof o === 'string' ? o : String(o)
    );

    const ctx = validateInstance(instance, res);
    if (!ctx) return;

    const jid = toJid(to);
    if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' });

    // Formato poll do InfiniteAPI/Baileys
    const result = await ctx.sock.sendMessage(jid, {
      poll: {
        name: String(name),            // pergunta exibida
        values: opts,                  // array de strings com as opções
        selectableCount: Math.min(
          Math.max(1, selectableCount ?? 1), // mínimo 1
          opts.length                         // máximo = total de opções
        ),
      },
    });

    return res.json({ ok: true, messageId: result?.key?.id });
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return res.status(500).json({ ok: false, error: message });
  }
});
```

**Payload de exemplo:**
```json
{
  "instance": "main",
  "to": "5511999999999",
  "name": "Qual seu framework favorito?",
  "options": ["React", "Vue", "Angular", "Svelte"],
  "selectableCount": 1
}
```

---

## 6. Carrossel

**Valor no select:** `carousel`  
**Endpoint:** `POST /v1/messages/send_carousel_helpers`  
**Descrição:** Mensagem com cards horizontais deslizáveis (`nativeCarousel`). Cada card pode ter título, corpo, imagem e botões próprios.

### Frontend — `public/index.html` (linhas 150–159)

```html
<!-- Formulário visível quando tipo = "carousel" -->
<div id="formCarousel" class="dispatch-form hidden">
  <!-- Texto opcional acima dos cards -->
  <div class="form-row">
    <label>Texto (acima dos cards)</label>
    <input type="text" id="carouselText" placeholder="Ofertas especiais">
  </div>

  <div class="form-row">
    <label>Rodapé</label>
    <input type="text" id="carouselFooter">
  </div>

  <!-- Cada card é um bloco com título, descrição, imagem e botões próprios -->
  <div class="form-row">
    <label>Cards</label>
    <div id="carouselCardsList" class="dynamic-list"></div>
    <button type="button" class="btn btn-small btn-ghost add-item" data-for="carouselCards">
      + Adicionar card
    </button>
  </div>
</div>
```

### Frontend — `public/app.js`

```js
// Cria um bloco-card com campos próprios (title, body, footer, imageUrl) + botões internos
function addCarouselCard() {
  const container = document.getElementById('carouselCardsList');
  const block = document.createElement('div');
  block.className = 'block-section';
  block.innerHTML = `
    <div class="block-title">Card</div>
    <div class="form-row"><input type="text" placeholder="Título"          data-field="title"></div>
    <div class="form-row"><input type="text" placeholder="Corpo/descrição" data-field="body"></div>
    <div class="form-row"><input type="text" placeholder="Rodapé"          data-field="footer"></div>
    <div class="form-row"><input type="text" placeholder="URL da imagem"   data-field="imageUrl"></div>
    <div class="sub-list card-buttons"></div>
    <button type="button" class="btn btn-small btn-ghost add-card-btn">+ Botão no card</button>
    <button type="button" class="btn btn-small btn-danger btn-remove-block">Remover card</button>
  `;

  // Botões dentro do card (quick reply específico do card)
  block.querySelector('.add-card-btn').addEventListener('click', () => {
    const row = document.createElement('div');
    row.className = 'item-row';
    row.innerHTML = `
      <input type="text" placeholder="ID"   data-field="id">
      <input type="text" placeholder="Texto" data-field="text">
      <button type="button" class="btn btn-small btn-ghost btn-remove">Remover</button>
    `;
    row.querySelector('.btn-remove').onclick = () => row.remove();
    block.querySelector('.card-buttons').appendChild(row);
  });

  block.querySelector('.btn-remove-block').onclick = () => block.remove();
  container.appendChild(block);
}

function getCarouselPayload() {
  const cards = [];

  document.querySelectorAll('#carouselCardsList .block-section').forEach((block) => {
    const title    = block.querySelector('[data-field="title"]')?.value?.trim();
    const body     = block.querySelector('[data-field="body"]')?.value?.trim();
    const footer   = block.querySelector('[data-field="footer"]')?.value?.trim();
    const imageUrl = block.querySelector('[data-field="imageUrl"]')?.value?.trim();
    const buttons  = [];

    // Coleta botões internos do card
    block.querySelectorAll('.card-buttons .item-row').forEach((row) => {
      const id   = row.querySelector('[data-field="id"]')?.value?.trim();
      const text = row.querySelector('[data-field="text"]')?.value?.trim();
      if (id && text) buttons.push({ id, text });
    });

    cards.push({
      title:    title    || '',
      body:     body     || '',
      footer:   footer   || undefined,
      imageUrl: imageUrl || undefined,
      // Fallback: ao menos 1 botão por card (exigência do InfiniteAPI)
      buttons: buttons.length ? buttons : [{ id: 'btn1', text: 'Ver' }],
    });
  });

  return {
    url: '/v1/messages/send_carousel_helpers',
    body: {
      instance: document.getElementById('dispatchInstance').value,
      to: document.getElementById('dispatchTo').value.trim(),
      text:   document.getElementById('carouselText').value.trim()   || undefined,
      footer: document.getElementById('carouselFooter').value.trim() || undefined,
      cards: cards.length ? cards : [
        { title: 'Card', body: '', buttons: [{ id: 'b1', text: 'Botão' }] }
      ],
    },
  };
}
```

### Backend — `src/routes/messages.ts` (linhas 238–289)

```ts
// POST /v1/messages/send_carousel_helpers
router.post('/send_carousel_helpers', async (req: Request, res: Response) => {
  try {
    const { instance = 'main', to, text, footer, cards } = req.body;

    if (!to || !Array.isArray(cards) || cards.length === 0) {
      return res.status(400).json({ ok: false, error: 'missing to/cards' });
    }

    // Limita a 10 cards (config.limits.maxCarouselCards)
    const limited = cards.slice(0, config.limits.maxCarouselCards);

    const ctx = validateInstance(instance, res);
    if (!ctx) return;

    const jid = toJid(to);
    if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' });

    // Formata cada card para o schema do InfiniteAPI/Baileys
    const formattedCards = limited.map((card, idx) => ({
      title:  card.title ?? `Card ${idx + 1}`,
      body:   card.body  ?? '',
      footer: card.footer,                                // opcional
      // Se imageUrl for fornecida, passa como { url } para o Baileys
      image:  card.imageUrl ? { url: card.imageUrl } : undefined,
      buttons: (card.buttons ?? []).map((btn, bIdx) => ({
        type: 'reply' as const,
        id:   btn.id   ?? `card${idx}_btn${bIdx}`,
        text: btn.text ?? 'Botão',
      })),
    }));

    // nativeCarousel = array de cards deslizáveis
    const result = await ctx.sock.sendMessage(jid, {
      nativeCarousel: { cards: formattedCards },
      text:   text   ? String(text)   : undefined,
      footer: footer ? String(footer) : undefined,
    });

    return res.json({ ok: true, format: 'nativeCarousel', messageId: result?.key?.id });
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return res.status(500).json({ ok: false, error: message });
  }
});
```

**Payload de exemplo:**
```json
{
  "instance": "main",
  "to": "5511999999999",
  "text": "🔥 Ofertas especiais de hoje:",
  "cards": [
    {
      "title": "Pizza Margherita",
      "body": "Molho, mozzarella, manjericão",
      "footer": "R$ 49,90",
      "imageUrl": "https://exemplo.com/pizza.jpg",
      "buttons": [{ "id": "pedir_pizza", "text": "🍕 Pedir agora" }]
    },
    {
      "title": "Combo Bebida",
      "body": "Refrigerante 2L + suco",
      "footer": "R$ 19,90",
      "buttons": [{ "id": "pedir_combo", "text": "🥤 Adicionar" }]
    }
  ]
}
```

---

## Arquivo de Limites — `src/config.ts`

```ts
export const config = {
  port:      parseInt(process.env.PORT ?? '8787', 10),
  apiKey:    process.env.API_KEY ?? 'ACFH4RFOTME4RU50R4FKGNW34LDFG8DSQ',
  authFolder: process.env.AUTH_FOLDER ?? 'auth',
  limits: {
    maxButtons:          3,   // botões quick reply / CTA (types 2 e 3)
    maxCarouselCards:   10,   // cards no carrossel (type 6)
    maxListSections:    10,   // seções na lista dropdown (type 4)
    maxListRowsPerSection: 10,// itens por seção na lista (type 4)
    maxPollOptions:     12,   // opções de enquete (type 5)
  },
} as const;
```

---

## Tipos TypeScript envolvidos — `src/types/whatsapp.ts`

```ts
// Tipos dos botões nativos (usados em "buttons" e "interactive")
interface NativeButtonReply { type: 'reply'; id: string; text: string }    // quick reply
interface NativeButtonUrl   { type: 'url';  text: string; url: string }    // abre link
interface NativeButtonCopy  { type: 'copy'; text: string; copyText: string } // copia texto
interface NativeButtonCall  { type: 'call'; text: string; phoneNumber: string } // disca

// MessageContent: tudo que pode ser passado para sock.sendMessage()
interface MessageContent {
  text?:          string;
  footer?:        string;
  nativeButtons?: NativeButton[];          // tipos 2 e 3
  nativeList?: {                           // tipo 4
    buttonText: string;
    sections: NativeListSection[];
  };
  nativeCarousel?: { cards: NativeCarouselCard[] }; // tipo 6
  poll?: {                                 // tipo 5
    name: string;
    values: string[];
    selectableCount?: number;
  };
}
```

---

## Helper `toJid` — `src/utils/helpers.ts`

```ts
// Converte número (+55 35 9882-8503 ou 553598828503) para JID do WhatsApp
export function toJid(phone: string | null | undefined): string | null {
  if (!phone || typeof phone !== 'string') return null;
  const cleaned = phone.replace(/\D/g, '');  // remove tudo que não é dígito
  if (cleaned.length < 10) return null;
  if (phone.includes('@')) return phone;      // já é JID, retorna como está
  return `${cleaned}@s.whatsapp.net`;
}
```
