Documentação / Mapeamento (Depara) - Pilot Status ↔ Evolution GO

Mapeamento (Depara) - Pilot Status ↔ Evolution GO

Entrar

Mapeamento (Depara) - Pilot Status ↔ Evolution GO

Este documento apresenta o mapeamento entre os eventos e campos da Pilot Status e a Evolution GO para facilitar a migração/integração entre as plataformas.


Visão Geral

| Aspecto | Pilot Status | Evolution GO | |---------|--------------|--------------| | Estrutura do payload | { "event": "...", "data": { ... } } | { "event": "...", "data": { ... } } | | Tipo de evento | Específico (ex: message.sent, message.reply) | Genérico (Message) com subtipos implícitos | | Ambientes | TEST e LIVE (campo data.environment) | Única instância (configurada na criação) | | Webhooks | Separados por ambiente | Separados por instance |


Mapeamento de Eventos

1. Envio de Mensagem

| Pilot Status | Evolution GO | Observações | |--------------|--------------|-------------| | message.sent | Message com FromMe: true | Evolution GO usa único evento "Message" para entrada e saída | | message.delivered | Receipt com state: "Delivered" | Disparado quando mensagem é entregue ao aparelho | | message.read | Receipt com state: "Read" ou "ReadSelf" | ReadSelf quando lida pelo próprio usuário | | message.failed | Não há evento específico | Verificar logs ou status HTTP no envio |

2. Recebimento de Mensagem

| Pilot Status | Evolution GO | Observações | |--------------|--------------|-------------| | message.received | Message com FromMe: false | Mensagem recebida sem contexto de resposta | | message.reply | Message com FromMe: false + quoted: { stanzaID: "..." } | Quando há contexto de resposta (quoted) |

3. Interações Especiais

| Pilot Status | Evolution GO | Observações | |--------------|--------------|-------------| | message.group | Message em grupo (@g.us) | Detectado pelo sufixo do campo Chat | | Não há equivalente | ButtonClick | Evento separado para cliques em botões interativos | | Não há equivalente | CallOffer | Evento para chamadas recebidas |

4. Outros Eventos

| Pilot Status | Evolution GO | Observações | |--------------|--------------|-------------| | Não há equivalente | Presence | Estado online/offline do contato | | Não há equivalente | ChatPresence | Presença no chat (compondo...) | | Não há equivalente | Qrcode | QR code para conexão | | Não há equivalente | Connected | Conexão estabelecida | | Não há equivalente | LoggedOut | Desconexão |


Mapeamento de Campos

Campos de Identificação de Mensagem

| Pilot Status | Evolution GO | Descrição | |--------------|--------------|-----------| | data.messageId | data.ID | ID da mensagem no WhatsApp | | data.internalMessageId | data.Id (na resposta do envio) | ID interno da mensagem na plataforma | | data.correlationId | Não há equivalente padrão | Use campo customizado no envio se necessário |

Campos de Resposta (Reply)

| Pilot Status | Evolution GO | Descrição | |--------------|--------------|-----------| | data.quotedMessageId | data.quoted.stanzaID | ID da mensagem original no WhatsApp que foi respondida | | data.messageRepliedId | Não há equivalente direto | Correlação interna deve ser feita via stanzaID | | data.replyContent | data.Message.conversation | Texto da resposta (quando applicable) | | data.content | data.Message.extendedTextMessage.text (na quoted) | Texto da mensagem original citada |

Campos de Dados da Mensagem

| Pilot Status | Evolution GO | Descrição | |--------------|--------------|-----------| | data.destinationNumber | data.Chat (formato JID) | Número de destino/remetente | | data.from | data.Sender | Número de quem enviou | | data.fromNumber | data.Sender | Número do remetente (formato completo) | | data.receivedAt | data.Timestamp | Timestamp da mensagem (Unix timestamp) | | data.environment | Configuração da instance | Ambiente (TEST/LIVE) | | data.content | Depende do tipo (data.Message.conversation, data.Message.extendedTextMessage.text, etc.) | Conteúdo da mensagem |

Campos de Grupo

| Pilot Status | Evolution GO | Descrição | |--------------|--------------|-----------| | data.groupId | data.Chat (quando termina em @g.us) | ID do grupo | | Não há equivalente | data.groupData | Metadados completos do grupo |


Exemplos Práticos

Envio de Mensagem (Pilot Status) → Envio na API Evolution GO

Pilot Status - Resposta ao enviar:

{
  "id": "cmn0qk33b001kmj01q37f6quv",
  "correlationId": "cid_d815edf55caf4b80a368932cf22cdfa6",
  "status": "QUEUED",
  "createdAt": "2026-04-16T15:00:00.000Z",
  "origin": "Meu WhatsApp"
}

Evolution GO - Endpoint de envio:

POST /message/send/text

Corpo da requisição:

{
  "number": "+5511999999999",
  "text": "Olá Maria, lembrete: consulta às 08:15.",
  "id": "cmn0qk33b001kmj01q37f6quv"
}

Mensagem Enviada (message.sent)

Pilot Status:

{
  "event": "message.sent",
  "data": {
    "messageId": "A2FHM8YGTQQQH992YRT4R",
    "internalMessageId": "cmn0qk33b001kmj01q37f6quv",
    "correlationId": "cid_d815edf55caf4b80a368932cf22cdfa6",
    "environment": "LIVE",
    "destinationNumber": "+5511999999999",
    "content": "Olá Maria, lembrete: consulta às 08:15.",
    "status": "SENT",
    "sentAt": "2026-04-16T15:00:03.000Z"
  }
}

Evolution GO:

{
  "event": "Message",
  "data": {
    "ID": "A2FHM8YGTQQQH992YRT4R",
    "Sender": "[email protected]",
    "Chat": "[email protected]",
    "FromMe": true,
    "Timestamp": 1713273603,
    "Message": {
      "conversation": "Olá Maria, lembrete: consulta às 08:15."
    }
  },
  "instanceId": "instance-uuid",
  "instanceName": "Minha Instância"
}

Correlação:

  • Pilot Status.messageIdEvolution GO.data.ID
  • Pilot Status.internalMessageIdid usado no envio via API Evolution GO

Mensagem Recebida como Resposta (message.reply)

Pilot Status:

{
  "event": "message.reply",
  "data": {
    "from": "+5511999999999",
    "fromNumber": "+5511999999999",
    "destinationNumber": "+552422317827",
    "content": "",
    "replyContent": "SIM",
    "receivedAt": "2026-04-16T15:41:07.445Z",
    "messageId": "ACFC4C4DCBEFA43466256BBB207FC3B0",
    "quotedMessageId": "A2FHM8YGTQQQH992YRT4R",
    "messageRepliedId": "cmn0qk33b001kmj01q37f6quv",
    "environment": "LIVE"
  }
}

Evolution GO:

{
  "event": "Message",
  "data": {
    "ID": "ACFC4C4DCBEFA43466256BBB207FC3B0",
    "Sender": "[email protected]",
    "Chat": "[email protected]",
    "FromMe": false,
    "Timestamp": 1713276667,
    "quoted": {
      "stanzaID": "A2FHM8YGTQQQH992YRT4R",
      "quotedMessage": {
        "conversation": "Olá Maria, lembrete: consulta às 08:15."
      }
    },
    "isQuoted": true,
    "Message": {
      "extendedTextMessage": {
        "text": "SIM"
      }
    }
  },
  "instanceId": "instance-uuid",
  "instanceName": "Minha Instância"
}

Correlação:

  • Pilot Status.replyContentEvolution GO.data.Message.extendedTextMessage.text
  • Pilot Status.quotedMessageIdEvolution GO.data.quoted.stanzaID
  • Pilot Status.messageRepliedId → Correlação deve ser feita buscando o envio original via stanzaID

Confirmação de Leitura (message.read)

Pilot Status:

{
  "event": "message.read",
  "data": {
    "messageId": "A2FHM8YGTQQQH992YRT4R",
    "internalMessageId": "cmn0qk33b001kmj01q37f6quv",
    "readAt": "2026-04-16T15:05:00.000Z",
    "environment": "LIVE"
  }
}

Evolution GO:

{
  "event": "Receipt",
  "data": {
    "messageIds": ["A2FHM8YGTQQQH992YRT4R"],
    "source": "[email protected]",
    "timestamp": 1713273900,
    "state": "Read"
  },
  "instanceId": "instance-uuid",
  "instanceName": "Minha Instância"
}

Correlação:

  • Pilot Status.messageIdEvolution GO.data.messageIds[0]
  • Pilot Status.readAtEvolution GO.data.timestamp

Clique em Botão Interativo

Pilot Status: Não há evento específico para cliques em botões

Evolution GO:

{
  "event": "ButtonClick",
  "data": {
    "buttonId": "btn_confirmar",
    "buttonText": "Confirmar",
    "type": "native_flow_response",
    "phone": "[email protected]",
    "jid": "[email protected]",
    "pushName": "Maria Silva",
    "messageId": "ABC123DEF456",
    "chat": "[email protected]",
    "fromMe": false,
    "timestamp": 1713276789,
    "extraData": {
      "name": "quick_reply",
      "paramsJSON": "{\"id\":\"btn_confirmar\",\"display_text\":\"Confirmar\"}"
    }
  },
  "instanceId": "instance-uuid",
  "instanceName": "Minha Instância"
}

Diferenças Importantes

1. Estrutura de Eventos

  • Pilot Status: Eventos granulares e específicos (message.sent, message.reply, etc.)
  • Evolution GO: Eventos mais genéricos (Message, Receipt) com informações que determinam o tipo

2. Correlação de Respostas

  • Pilot Status: Campos dedicados quotedMessageId e messageRepliedId
  • Evolution GO: Apenas quoted.stanzaID (ID do WhatsApp). A correlação interna deve ser implementada pelo consumidor do webhook

3. Identificador Interno

  • Pilot Status: internalMessageId retorna sempre em webhooks de mensagem
  • Evolution GO: O id fornecido no envio via API não retorna automaticamente no webhook. Use ID (do WhatsApp) como chave principal

4. Ambiente

  • Pilot Status: Campo data.environment indica TEST ou LIVE
  • Evolution GO: Cada instance pode ter configuração diferente, mas não há indicação de ambiente no payload

5. Eventos de Botão

  • Pilot Status: Não há evento específico para cliques em botões
  • Evolution GO: Evento separado ButtonClick com detalhes do botão clicado

Estratégia de Migração

1. Mapeamento de Identificadores

// Ao enviar mensagem em Evolution GO
const response = await fetch('/message/send/text', {
  method: 'POST',
  body: JSON.stringify({
    number: '+5511999999999',
    text: 'Olá Maria',
    id: 'seu-id-interno-unico'  // Use como correlationId
  })
});

const sentData = await response.json();
// Guarde: sentData.key.id = ID do WhatsApp (equivale a messageId da Pilot Status)

2. Tratamento de Respostas

// No webhook do Evolution GO
if (event === 'Message' && data.isQuoted) {
  // data.quoted.stanzaID = quotedMessageId da Pilot Status
  // Busque no seu sistema a mensagem original usando stanzaID
  const mensagemOriginal = await buscarMensagemPorWhatsAppId(data.quoted.stanzaID);
  
  // data.Message.conversation = replyContent da Pilot Status
  const resposta = data.Message.conversation || data.Message.extendedTextMessage?.text;
}

3. Tratamento de Status

if (event === 'Receipt') {
  if (data.state === 'Read') {
    // message.read da Pilot Status
  } else if (data.state === 'Delivered') {
    // message.delivered da Pilot Status
  }
}

4. Diferenciação de Tipos de Mensagem

if (event === 'Message') {
  if (data.FromMe) {
    // Mensagem enviada por você (não há equivalente direto na Pilot Status)
  } else if (data.isQuoted) {
    // message.reply da Pilot Status
  } else {
    // message.received da Pilot Status
  }
}

Campos Adicionais da Evolution GO (Não presentes na Pilot Status)

| Campo | Descrição | |-------|-----------| | data.PushName | Nome de exibição do contato no WhatsApp | | data.groupData | Metadados completos do grupo (quando aplicável) | | data.isQuoted | Flag indicando se a mensagem é uma resposta | | data.Message.<tipo> | Estrutura detalhada por tipo de mensagem (imageMessage, videoMessage, etc.) | | data.quoted.quotedMessage | Objeto completo da mensagem citada |


Eventos de Grupo e Newsletter

A Evolution GO trata mensagens de grupos e newsletters (canais) de forma específica. Ambos usam o mesmo evento base Message, mas podem ser diferenciados pelo formato do campo Chat (JID) e pelos dados adicionais incluídos no payload.


Identificação por JID (Chat)

O campo data.Info.Chat (ou data.Chat dependendo do contexto) indica o tipo de conversa:

| Tipo | Formato do JID | Exemplo | |------|----------------|---------| | Chat individual | {numero}@s.whatsapp.net | [email protected] | | Grupo | {id_do_grupo}@g.us | [email protected] | | Newsletter/Canal | {id_do_newsletter}@newsletter | 123456789@newsletter | | Status/Broadcast | status@broadcast | status@broadcast |


Mensagem em Grupo (Webhook)

Quando alguém envia uma mensagem em um grupo, o webhook recebe o evento Message com informações adicionais do grupo.

Payload de Mensagem em Grupo

{
  "event": "Message",
  "data": {
    "Info": {
      "MessageSource": {
        "Chat": "[email protected]",
        "Sender": "[email protected]",
        "SenderAlt": "5511999999999@lid",
        "IsFromMe": false,
        "IsGroup": true
      },
      "ID": "ACFC4C4DCBEFA43466256BBB207FC3B0",
      "Timestamp": 1713276667,
      "Type": "ImageMessage",
      "PushName": "Maria Silva"
    },
    "Message": {
      "conversation": "Olá pessoal!"
    },
    "groupData": {
      "JID": "[email protected]",
      "OwnerJID": "[email protected]",
      "GroupName": {
        "Name": "Grupo Vendas",
        "ID": "nome-grupo-id"
      },
      "GroupTopic": {
        "Topic": "Canal de vendas da empresa",
        "ID": "topico-id"
      },
      "Participants": [
        {
          "JID": "[email protected]",
          "IsAdmin": true,
          "IsSuperAdmin": false
        },
        {
          "JID": "[email protected]",
          "IsAdmin": false,
          "IsSuperAdmin": false
        }
      ],
      "GroupCreated": 1710000000
    },
    "isQuoted": false
  },
  "instanceToken": "api-token-12345",
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "instanceName": "Atendimento"
}

Campos Específicos de Grupo

| Campo | Descrição | |-------|-----------| | data.Info.Chat | JID do grupo (termina com @g.us) | | data.Info.IsGroup | true quando é mensagem de grupo | | data.Info.Sender | JID do participante que enviou a mensagem | | data.Info.PushName | Nome de exibição do participante no WhatsApp | | data.groupData | Metadados completos do grupo |

Estrutura de groupData

| Campo | Descrição | |-------|-----------| | groupData.JID | JID do grupo | | groupData.OwnerJID | JID do dono/administrador do grupo | | groupData.GroupName.Name | Nome do grupo | | groupData.GroupTopic.Topic | Descrição/tópico do grupo | | groupData.Participants | Lista de participantes com status de admin | | groupData.Participants[].JID | JID do participante | | groupData.Participants[].IsAdmin | Se o participante é admin | | groupData.Participants[].IsSuperAdmin | Se o participante é super admin | | data.GroupCreated | Timestamp de criação do grupo |


Mensagem em Newsletter / Canal (Webhook)

Quando alguém envia uma mensagem em um newsletter (canal), o webhook recebe o evento Message com o Chat terminando em @newsletter.

Payload de Mensagem em Newsletter

{
  "event": "Message",
  "data": {
    "Info": {
      "MessageSource": {
        "Chat": "123456789@newsletter",
        "Sender": "[email protected]",
        "IsFromMe": false,
        "IsGroup": false
      },
      "ID": "NEWSLETTER_MSG_ABC123",
      "Timestamp": 1713277000,
      "Type": "ExtendedTextMessage"
    },
    "Message": {
      "extendedTextMessage": {
        "text": "Confira as novidades do canal!"
      }
    },
    "isQuoted": false
  },
  "instanceToken": "api-token-12345",
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "instanceName": "Atendimento"
}

Campos Específicos de Newsletter

| Campo | Descrição | |-------|-----------| | data.Info.Chat | JID do newsletter (termina com @newsletter) | | data.Info.Sender | JID do autor da mensagem no canal | | data.Info.IsGroup | false (newsletters não são grupos) |


Eventos Específicos de Grupo

Além das mensagens, a Evolution GO emite eventos quando a instância se junta a um grupo ou quando as informações do grupo são atualizadas.

Evento: JoinedGroup

{
  "event": "JoinedGroup",
  "data": {
    "JID": "[email protected]",
    "OwnerJID": "[email protected]",
    "GroupName": {
      "Name": "Grupo Vendas"
    }
  },
  "instanceToken": "api-token-12345",
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "instanceName": "Atendimento"
}

Evento: GroupInfo

{
  "event": "GroupInfo",
  "data": {
    "JID": "[email protected]",
    "OwnerJID": "[email protected]",
    "GroupName": {
      "Name": "Grupo Vendas"
    },
    "GroupTopic": {
      "Topic": "Canal de vendas"
    }
  },
  "instanceToken": "api-token-12345",
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "instanceName": "Atendimento"
}

Eventos Específicos de Newsletter

Evento: NewsletterJoin

{
  "event": "NewsletterJoin",
  "data": {
    "JID": "123456789@newsletter",
    "Name": "Canal de Novidades"
  },
  "instanceToken": "api-token-12345",
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "instanceName": "Atendimento"
}

Evento: NewsletterLeave

{
  "event": "NewsletterLeave",
  "data": {
    "JID": "123456789@newsletter"
  },
  "instanceToken": "api-token-12345",
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "instanceName": "Atendimento"
}

Configuração de Assinatura de Eventos

Para receber eventos de grupo e newsletter, configure a assinatura ao conectar a instância:

{
  "subscribe": ["MESSAGE", "GROUP", "NEWSLETTER"]
}

Tipos de Eventos Disponíveis

| Tipo | Eventos Incluídos | Descrição | |------|-------------------|-----------| | MESSAGE | Message | Todas as mensagens (inclui grupos e newsletters automaticamente) | | GROUP | JoinedGroup, GroupInfo + mensagens em grupo (fallback) | Eventos específicos de grupo | | NEWSLETTER | NewsletterJoin, NewsletterLeave + mensagens em newsletter (fallback) | Eventos específicos de newsletter |

Comportamento importante: Se você assinar apenas GROUP (sem MESSAGE), ainda receberá mensagens em grupos. Se assinar apenas NEWSLETTER (sem MESSAGE), ainda receberá mensagens em newsletters. O sistema faz fallback automático para GROUP e NEWSLETTER quando o Chat termina em @g.us ou @newsletter.


Comparação com Pilot Status

Mensagens em Grupo

Pilot Status (message.group):

{
  "event": "message.group",
  "data": {
    "messageId": "ABC123",
    "from": "+5511999999999",
    "groupId": "[email protected]",
    "content": "Olá pessoal!",
    "environment": "LIVE"
  }
}

Evolution GO (Message em grupo):

{
  "event": "Message",
  "data": {
    "Info": {
      "Chat": "[email protected]",
      "Sender": "[email protected]",
      "IsFromMe": false,
      "IsGroup": true
    },
    "Message": {
      "conversation": "Olá pessoal!"
    },
    "groupData": { ... }
  }
}

Diferenças:

| Aspecto | Pilot Status | Evolution GO | |----------|-------------|--------------| | Evento | message.group (dedicado) | Message (genérico, identificar por @g.us) | | ID do grupo | data.groupId | data.Info.Chat | | Remetente | data.from (formato telefone) | data.Info.Sender (formato JID) | | Metadados do grupo | Não incluídos | data.groupData completo | | Nome do remetente | Não incluído | data.Info.PushName |

Mensagens em Newsletter/Canal

Pilot Status: Não possui suporte a newsletters/canais.

Evolution GO: Suporte completo com eventos NewsletterJoin, NewsletterLeave e mensagens em newsletters via Message com Chat terminando em @newsletter.


Boas Práticas

1. Diferenciar Tipo de Conversa

function identificarTipoConversa(webhookData) {
  const chat = webhookData.data?.Info?.Chat || webhookData.data?.Chat || '';
  
  if (chat.endsWith('@g.us')) {
    return 'group';
  } else if (chat.endsWith('@newsletter')) {
    return 'newsletter';
  } else if (chat === 'status@broadcast') {
    return 'status';
  } else if (chat.endsWith('@s.whatsapp.net')) {
    return 'private';
  } else {
    return 'unknown';
  }
}

// Uso
app.post('/webhook', (req, res) => {
  const tipo = identificarTipoConversa(req.body);
  
  switch (tipo) {
    case 'group':
      console.log(`Mensagem no grupo: ${req.body.data?.groupData?.GroupName?.Name}`);
      processarMensagemGrupo(req.body);
      break;
    case 'newsletter':
      console.log(`Mensagem no canal: ${req.body.data?.Info?.Chat}`);
      processarMensagemNewsletter(req.body);
      break;
    case 'private':
      console.log(`Mensagem privada de: ${req.body.data?.Info?.Sender}`);
      processarMensagemPrivada(req.body);
      break;
    case 'status':
      console.log('Status/broadcast ignorado');
      break;
  }

  res.sendStatus(200);
});

2. Extrair Número de Telefone do JID

function extrairNumero(jid) {
  if (!jid) return '';
  // Remove o sufixo (@s.whatsapp.net, @g.us, @newsletter, etc.)
  return jid.split('@')[0];
}

// Uso
const numeroRemetente = extrairNumero('[email protected]');
// Resultado: "5511999999999"

const idGrupo = extrairNumero('[email protected]');
// Resultado: "120363abcxyz"

3. Processar Mensagens de Grupo com Contexto

async function processarMensagemGrupo(webhookData) {
  const info = webhookData.data?.Info;
  const groupData = webhookData.data?.groupData;

  const mensagem = {
    // Identificação
    messageId: info?.ID,
    timestamp: info?.Timestamp,
    
    // Grupo
    grupoJid: info?.Chat,
    grupoNome: groupData?.GroupName?.Name,
    grupoTopico: groupData?.GroupTopic?.Topic,
    grupoDono: extrairNumero(groupData?.OwnerJID),
    
    // Remetente
    remetenteJid: info?.Sender,
    remetenteNumero: extrairNumero(info?.Sender),
    remetenteNome: info?.PushName,
    
    // Conteúdo
    tipoMensagem: info?.Type,
    conteudo: extrairConteudo(webhookData.data?.Message),
    
    // Resposta (quoted)
    isResposta: webhookData.data?.isQuoted || false,
    mensagemOriginalId: webhookData.data?.quoted?.stanzaID,
  };

  // Verificar se o remetente é admin
  if (groupData?.Participants) {
    const participante = groupData.Participants.find(
      p => p.JID === info?.Sender
    );
    mensagem.remetenteIsAdmin = participante?.IsAdmin || false;
  }

  return mensagem;
}

function extrairConteudo(message) {
  if (!message) return '';
  
  if (message.conversation) return message.conversation;
  if (message.extendedTextMessage?.text) return message.extendedTextMessage.text;
  if (message.imageMessage) return `[Imagem: ${message.imageMessage.caption || 'sem legenda'}]`;
  if (message.videoMessage) return `[Vídeo: ${message.videoMessage.caption || 'sem legenda'}]`;
  if (message.documentMessage) return `[Documento: ${message.documentMessage.fileName || 'sem nome'}]`;
  if (message.audioMessage) return '[Áudio]';
  if (message.stickerMessage) return '[Sticker]';
  if (message.pollCreationMessage) return `[Enquete: ${message.pollCreationMessage.name}]`;
  
  return '[Mensagem não suportada]';
}

4. Ignorar Mensagens de Grupos/Newsletters

// Se configurado para ignorar grupos/newsletters no webhook
app.post('/webhook', (req, res) => {
  const tipo = identificarTipoConversa(req.body);
  
  // Filtrar tipos indesejados
  if (tipo === 'group' && process.env.IGNORE_GROUPS === 'true') {
    return res.sendStatus(200);
  }
  
  if (tipo === 'newsletter' && process.env.IGNORE_NEWSLETTERS === 'true') {
    return res.sendStatus(200);
  }
  
  if (tipo === 'status') {
    return res.sendStatus(200);
  }
  
  // Processar apenas mensagens privadas
  processarMensagem(req.body);
  res.sendStatus(200);
});

Nota: A Evolution GO também suporta ignorar grupos diretamente na configuração da instância via advancedSettings.ignoreGroups: true ou via configuração global do servidor.

5. Enviar Mensagem para Grupo

async function enviarMensagemGrupo(instanceId, grupoJid, texto) {
  return await enviarTexto(instanceId, {
    number: grupoJid, // Usa o JID completo do grupo ([email protected])
    text: texto
  });
}

// Uso
await enviarMensagemGrupo(
  'instance-uuid',
  '[email protected]',
  'Olá grupo! Esta é uma mensagem do bot.'
);

Nota: Para grupos, use o JID completo (incluindo @g.us) como número de destino. Para mencionar todos os participantes, use mentionAll: true.


Configuração de Webhooks

Pilot Status

  • URL configurada por ambiente (TEST e LIVE separados)
  • Eventos selecionados individualmente

Evolution GO

  • Webhook configurado por instance
  • Eventos configurados via parâmetro events (ex: MESSAGE,RECEIPT,BUTTON_CLICK)
  • Se não especificado, assume MESSAGE como padrão

Considerações Finais

  1. Correlação: A Evolution GO não fornece correlação interna automática. Implemente seu próprio mapeamento usando o ID do WhatsApp como chave.

  2. Eventos: A Pilot Status tem eventos mais granulares, enquanto Evolution GO agrupa múltiplos tipos em eventos genéricos.

  3. Botões: A Evolution GO trata cliques em botões como evento separado (ButtonClick), algo que não existe na Pilot Status.

  4. Mídia: Ambas as plataformas suportam mídia, mas a estrutura do payload difere.

  5. Testes: Recomenda-se implementar um consumidor de webhooks que traduza entre os formatos durante a fase de migração.


Endpoints de Envio de Mídia - Evolution GO

Visão Geral

A Evolution GO fornece endpoints para envio de diferentes tipos de mídia (imagem, vídeo, PDF, etc.). O endpoint principal é o /send/media, que suporta múltiplos formatos de envio.


Endpoint: POST /send/media

Descrição: Envia mensagem com mídia (imagem, vídeo, áudio, documento/PDF).

Suporta dois formatos de envio:

  1. multipart/form-data - Upload direto do arquivo
  2. application/json - URL ou base64 da mídia

1. Enviando Imagem

via multipart/form-data

POST /send/media
Content-Type: multipart/form-data

Campos do formulário: | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | number | string | ✅ Sim | Número de telefone (com DDI, ex: 5511999999999) | | file | file | ✅ Sim | Arquivo de imagem (jpg, png, webp) | | type | string | ✅ Sim | Tipo de mídia: "image" | | caption | string | ❌ Não | Legenda da imagem | | filename | string | ❌ Não | Nome do arquivo | | id | string | ❌ Não | ID customizado da mensagem | | delay | integer | ❌ Não | Delay em milissegundos antes do envio | | mentionAll | string | ❌ Não | true para mencionar todos | | mentionedJid | string[] | ❌ Não | Lista de JIDs para mencionar | | quoted.messageId | string | ❌ Não | ID da mensagem para responder (reply-to) | | quoted.participant | string | ❌ Não | Participante da mensagem citada (para grupos) |

Exemplo com cURL:

curl -X POST "http://localhost:3000/instance/{instanceId}/send/media" \
  -F "number=5511999999999" \
  -F "type=image" \
  -F "file=@/path/to/image.jpg" \
  -F "caption=Olá! Confira esta imagem" \
  -F "id=msg-custom-001"

via JSON com URL

POST /send/media
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999",
  "url": "https://exemplo.com/imagem.jpg",
  "type": "image",
  "caption": "Olá! Confira esta imagem",
  "id": "msg-custom-001"
}

via JSON com Base64

POST /send/media
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999",
  "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD...",
  "type": "image",
  "caption": "Olá! Imagem em base64"
}

2. Enviando Vídeo

via multipart/form-data

POST /send/media
Content-Type: multipart/form-data

Campos do formulário: | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | number | string | ✅ Sim | Número de telefone | | file | file | ✅ Sim | Arquivo de vídeo (mp4) | | type | string | ✅ Sim | Tipo de mídia: "video" | | caption | string | ❌ Não | Legenda do vídeo | | filename | string | ❌ Não | Nome do arquivo | | id | string | ❌ Não | ID customizado | | delay | integer | ❌ Não | Delay em milissegundos |

Exemplo com cURL:

curl -X POST "http://localhost:3000/instance/{instanceId}/send/media" \
  -F "number=5511999999999" \
  -F "type=video" \
  -F "file=@/path/to/video.mp4" \
  -F "caption=Assista este vídeo!"

via JSON com URL

{
  "number": "5511999999999",
  "url": "https://exemplo.com/video.mp4",
  "type": "video",
  "caption": "Assista este vídeo!"
}

3. Enviando PDF/Documento

via multipart/form-data

POST /send/media
Content-Type: multipart/form-data

Campos do formulário: | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | number | string | ✅ Sim | Número de telefone | | file | file | ✅ Sim | Arquivo PDF (ou outro documento) | | type | string | ✅ Sim | Tipo de mídia: "document" | | filename | string | ❌ Não | Nome do arquivo (ex: contrato.pdf) | | caption | string | ❌ Não | Legenda do documento | | id | string | ❌ Não | ID customizado |

Exemplo com cURL:

curl -X POST "http://localhost:3000/instance/{instanceId}/send/media" \
  -F "number=5511999999999" \
  -F "type=document" \
  -F "file=@/path/to/documento.pdf" \
  -F "filename=contrato.pdf" \
  -F "caption=Aqui está seu contrato"

via JSON com URL

{
  "number": "5511999999999",
  "url": "https://exemplo.com/documento.pdf",
  "type": "document",
  "filename": "contrato.pdf",
#### via JSON com URL

```json
{
  "number": "5511999999999",
  "url": "https://exemplo.com/documento.pdf",
  "type": "document",
  "filename": "contrato.pdf",
  "caption": "Aqui está seu contrato"
}

Endpoint: POST /send/audio

Descrição: Envia mensagem de áudio para o WhatsApp. O endpoint converte automaticamente o áudio para o formato suportado pelo WhatsApp (.ogg com codec Opus) antes de enviar.

Endpoint:

POST /send/audio
Content-Type: application/json

Campos Obrigatórios

| Campo | Tipo | Descrição | |-------|------|-----------| | number | string | Número de telefone (com DDI, ex: 5511999999999) | | url | string | URL do áudio (MP3, WAV, OGG, etc.) | | id | string | ID customizado da mensagem |

Campo Opcional

| Campo | Tipo | Padrão | Descrição | |-------|------|---------|-----------| | delay | integer | null | Tempo em milissegundos antes de enviar (simula "gravando áudio...") | | quoted.messageId | string | null | ID da mensagem original para responder | | quoted.participant | string | null | Participante (para resposta em grupo) | | mentionedJid | string[] | [] | Lista de JIDs para mencionar | | mentionAll | boolean | false | Mencionar todos (para grupos) | | formatJid | boolean | null | Formatar JID para visualização |


Funcionamento da Conversão de Áudio

Ao enviar um áudio, o sistema executa automaticamente:

  1. Detecta o tipo de conversão:

    • API externa (MinIO): Se config.ApiAudioConverter estiver configurado
    • Conversão local (FFmpeg): Se ffmpeg estiver disponível no sistema
  2. Conversão via API (MinIO):

    • Faz POST multipart para a URL configurada
    • Envia o áudio como base64 no campo base64 OU como URL no campo url
    • Recebe o áudio convertido em formato .ogg (Opus codec)
    • Recebe a duração do áudio em segundos
  3. Conversão via FFmpeg (local):

    • Executa FFmpeg para converter para .ogg com codec Opus
    • Comandos: ffmpeg -i input.mp3 -f ogg -vn -acodec libopus -b:a 128k output.ogg
    • Detecta automaticamente a duração do áudio
  4. Upload para o WhatsApp:

    • Envia o áudio convertido para os servidores do WhatsApp
    • Formato: audio/ogg; codecs=opus
    • Para newsletters: envia sem criptografia
    • Para chats normais: envia com criptografia E2E
  5. Exibe "gravando áudio...":

    • Antes do envio, envia estado composing com mídia audio
    • Aguarda o tempo configurado no campo delay
    • Envia a mensagem
    • Envia estado paused para parar o indicador

Exemplos de Uso

Exemplo 1: Enviar áudio com URL

POST /send/audio
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999",
  "url": "https://exemplo.com/audio-mensagem.mp3",
  "id": "audio-001"
}

Exemplo 2: Enviar áudio com simulação de gravação

POST /send/audio
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999",
  "url": "https://exemplo.com/resposta-voz.ogg",
  "delay": 3000,
  "id": "audio-002"
}

Comportamento:

  1. Exibe "gravando áudio..." por 3 segundos
  2. Envia o áudio
  3. Para o indicador

Exemplo 3: Respondendo a mensagem com áudio

POST /send/audio
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999",
  "url": "https://exemplo.com/resposta.ogg",
  "delay": 2000,
  "quoted": {
    "messageId": "ABC123XYZ",
    "participant": "[email protected]"
  },
  "id": "audio-resposta-001"
}

Exemplo com cURL

curl -X POST "http://localhost:3000/instance/{instanceId}/send/audio" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "5511999999999",
    "url": "https://exemplo.com/mensagem.mp3",
    "delay": 2000,
    "id": "audio-001"
  }'

Resposta de Sucesso (HTTP 200)

{
  "message": "success",
  "data": {
    "Info": {
      "MessageSource": {
        "Chat": "[email protected]",
        "Sender": "[email protected]",
        "IsFromMe": true,
        "IsGroup": false
      },
      "ID": "3EB0ABCDEF1234567890",
      "Timestamp": 1713993600000,
      "ServerID": 1234567890,
      "Type": "AudioMessage"
    },
    "Message": {
      "audioMessage": {
        "URL": "https://mmg.whatsapp.net/v/t62.abc123/audio.ogg",
        "DirectPath": "[email protected]",
        "MediaKey": "base64-encoded-key",
        "Mimetype": "audio/ogg; codecs=opus",
        "FileEncSHA256": "abc123...",
        "FileLength": 456789,
        "PTT": true,
        "Seconds": 30
      },
      "ContextInfo": {
        "QuotedMessage": null
      }
    }
  },
  "instanceToken": "api-token-12345",
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "instanceName": "Atendimento"
}

Campos da Resposta de Áudio

| Campo | Descrição | |-------|-----------| | data.Message.audioMessage.URL | URL do áudio nos servidores do WhatsApp | | data.Message.audioMessage.DirectPath | Caminho direto do arquivo original | | data.Message.audioMessage.MediaKey | Chave de criptografia da mídia | | data.Message.audioMessage.Mimetype | Tipo MIME (sempre audio/ogg; codecs=opus) | | data.Message.audioMessage.FileEncSHA256 | SHA256 do arquivo criptografado | | data.Message.audioMessage.FileLength | Tamanho do arquivo em bytes | | data.Message.audioMessage.Seconds | Duração do áudio em segundos | | data.Message.audioMessage.PTT | Push-to-talk (true) ou mensagem comum (false) | | data.Message.audioMessage.Waveform | Dados de visualização de onda (Ogg) |


Configuração de Conversão de Áudio

A Evolution GO suporta dois métodos de conversão de áudio para o formato .ogg (Opus):

1. API Externa (MinIO)

Configure no arquivo de configuração:

# config/config.yaml
ApiAudioConverter: "https://api.minio.io/convert"
ApiAudioConverterKey: "sua-chave-api-minio"

Vantagens:

  • Não requer FFmpeg instalado no servidor
  • Processamento rápido via API dedicada
  • Escalável (processamento em nuvem)

Desvantagens:

  • Dependência de serviço externo
  • Custo de chamadas da API
2. Conversão Local (FFmpeg)

Se a API externa não estiver configurada, o sistema tenta usar FFmpeg local.

Requisitos do Sistema:

  • FFmpeg instalado e no PATH
  • Bibliotecas: libopus, libvorbis, etc.

Parâmetros de Conversão:

ffmpeg -i input.{ext} -f ogg -vn -acodec libopus -b:a 128k -ar 48000 output.ogg

| Parâmetro | Descrição | |-----------|-----------| | -i input.{ext} | Arquivo de entrada (qualquer formato suportado) | | -f ogg | Formato de saída Ogg | | -vn | Sem vídeo (audio-only) | | -acodec libopus | Codec Opus | | -b:a 128k | Taxa de bits (128 kbps) | | -ar 48000 | Taxa de amostragem (48 kHz - padrão WhatsApp) | | output.ogg | Arquivo de saída |


Mapeamento de Erros

| Erro | Status HTTP | Mensagem | Causa | Solução | |-------|-------------|----------|--------|----------| | Número obrigatório | 400 | "phone number is required" | Campo number não enviado | Incluir o número de telefone | | URL obrigatória | 400 | "url is required" | Campo url não enviado | Incluir a URL do áudio | | Instância não encontrada | 500 | "instance not found" | Instance ID inválido ou token incorreto | Verificar o ID e token da instância | | Cliente desconectado | 500 | "client disconnected" | Instância não está conectada ao WhatsApp | Conectar a instância primeiro | | Falha ao baixar áudio | 500 | Erro ao baixar a URL | Verificar se a URL está acessível | | Falha na conversão (FFmpeg) | 500 | "failed to convert audio to opus" | Erro na conversão local | 1. Verificar se FFmpeg está instalado<br>2. Verificar formato do arquivo | | Falha na conversão (API) | 500 | "failed to convert audio: ..." | Erro na conversão externa | 1. Verificar configuração da API<br>2. Verificar a URL de conversão | | Formato não suportado | 500 | "invalid media type" | Formato de áudio inválido após conversão | Use formatos compatíveis (MP3, WAV, OGG) | | Documento em newsletter | 500 | "documentos não são suportados em canais do WhatsApp" | Enviando áudio como documento para newsletter | Não enviar documentos para newsletters |


Comportamento com Newsletters

Ao enviar áudio para um newsletter (canal do WhatsApp, @newsletter), o sistema:

  1. Converte o áudio para formato .ogg
  2. Envia sem criptografia (upload UploadNewsletter)
  3. Não adiciona PTT (Push-to-Talk) na mensagem
  4. O áudio é tratado como mensagem normal (não como nota de voz)

Boas Práticas

1. Enviar Áudio com Simulação de "Gravando Áudio"

async function enviarAudioComSimulacao(instanceId, numero, audioUrl, duracao) {
  try {
    const response = await fetch(
      `http://localhost:3000/instance/${instanceId}/send/audio`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          number: numero,
          url: audioUrl,
          delay: duracao * 1000 // Delay em milissegundos
        })
      }
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Erro: ${error.error}`);
    }

    const result = await response.json();
    console.log(`Áudio enviado com ID: ${result.data.Info.ID}`);
    return result.data;

  } catch (error) {
    console.error('Erro ao enviar áudio:', error.message);
    throw error;
  }
}

// Uso: Envia áudio e exibe "gravando áudio..." por 5 segundos antes do envio
await enviarAudioComSimulacao('instance-uuid', '5511999999999', 'https://exemplo.com/mensagem.ogg', 5);

2. Enviar Áudio como Resposta

async function responderComAudio(instanceId, numero, audioUrl, mensagemOriginalId) {
  const response = await fetch(
    `http://localhost:3000/instance/${instanceId}/send/audio`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        number: numero,
        url: audioUrl,
        delay: 1000,
        quoted: {
          messageId: mensagemOriginalId,
          participant: null // Opcional, usado em grupos
        }
      })
    }
  );

  const result = await response.json();
  return result.data;
}

// Uso
await responderComAudio('instance-uuid', '5511999999999', 'https://exemplo.com/resposta.ogg', 'msg-abc-123');

3. Verificar Formatos Suportados

// Formatos que geralmente funcionam bem
const formatosRecomendados = [
  'mp3',
  'wav',
  'ogg'  // Já convertido
];

// Formatos que podem ter problemas
const formatosComProblemas = [
  'm4a',      // Requer conversão mais complexa
  'aac',       // Pode não ser suportado
  'flac',      // Tamanho muito grande
  'wma'        // Proprietário
];

function verificarFormato(arquivo) {
  const extensao = arquivo.split('.').pop().toLowerCase();
  
  if (formatosComProblemas.includes(extensao)) {
    console.warn(`Formato ${extensao.toUpperCase()} pode não ser suportado ou ter problemas.`);
    console.warn('Recomenda converter para MP3 ou WAV antes de enviar.');
  }
}

4. Enviar Áudio para Newsletter (Canal)

async function enviarAudioNewsletter(instanceId, newsletterJid, audioUrl) {
  const response = await fetch(
    `http://localhost:3000/instance/${instanceId}/send/audio`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        number: newsletterJid, // JID do newsletter termina em @newsletter
        url: audioUrl,
        delay: 2000
      })
    }
  );

  const result = await response.json();
  return result.data;
}

// Nota: Áudios em newsletters são enviados sem PTT (não são notas de voz)

5. Monitoramento de Duração do Áudio

// Enviar áudio e acompanhar a duração
async function enviarComMonitoramento(instanceId, numero, audioUrl) {
  const inicio = Date.now();
  
  const result = await enviarAudio(instanceId, numero, audioUrl);
  const duracao = result.data.Message.audioMessage.Seconds;
  
  const fim = Date.now();
  const tempoTotal = (fim - inicio) / 1000; // em segundos
  
  console.log(`Áudio enviado:`);
  console.log(`  - Duração do áudio: ${duracao}s`);
  console.log(`  - Tempo de processamento: ${tempoTotal}s`);
  console.log(`  - Overhead de conversão: ${tempoTotal - duracao}s`);
  
  // Alerta se o tempo de processamento for muito alto
  if (tempoTotal > duracao + 10) {
    console.warn(`ALERTA: Processamento levou ${tempoTotal}s para um áudio de ${duracao}s`);
  }
  
  return result;
}

Comparação com Pilot Status

Pilot Status:

  • Não possui endpoint dedicado para áudio
  • Áudios são enviados como documentos anexados
  • Sem suporte a conversão automática
  • Dependência de cliente converter para .ogg antes

Evolution GO:

  • Endpoint dedicado /send/audio
  • Conversão automática para .ogg (Opus)
  • Suporte a API externa (MinIO) ou FFmpeg local
  • Simulação de "gravando áudio..." via delay ou ChatPresence
  • Envia em formato PTT (nota de voz) ou normal

Vantagem: O usuário não precisa converter manualmente para .ogg, o sistema faz automaticamente. }

---

### Método 1: Endpoint Manual (POST /message/presence)

**Descrição:** Envia um sinal de presença no chat (digitando ou gravando áudio) para um número específico. Exibe o indicador "digitando..." ou "gravando áudio..." no WhatsApp do destinatário.

**Endpoint:**
```http
POST /message/presence
Content-Type: application/json

Campos Obrigatórios

| Campo | Tipo | Descrição | |-------|------|-----------| | number | string | Número de telefone (com DDI, ex: 5511999999999) | | state | string | Estado da presença: "composing" ou "paused" |

Campo Opcional

| Campo | Tipo | Padrão | Descrição | |-------|------|---------|-----------| | isAudio | boolean | false | Se true, exibe "gravando áudio..." em vez de "digitando..." |

Valores de state

| Valor | Efeito no WhatsApp | |-------|-------------------| | composing | Exibe "digitando..." (texto) ou "gravando áudio..." (se isAudio: true) | | paused | Remove o indicador de digitando/gravando |


Exemplo 1: Simular "digitando..."

POST /message/presence
Content-Type: application/json
{
  "number": "5511999999999",
  "state": "composing"
}

Exemplo 2: Simular "gravando áudio..."

POST /message/presence
Content-Type: application/json
{
  "number": "5511999999999",
  "state": "composing",
  "isAudio": true
}

Exemplo 3: Parar de digitar

POST /message/presence
Content-Type: application/json
{
  "number": "5511999999999",
  "state": "paused"
}

Exemplo com cURL

curl -X POST "http://localhost:3000/instance/{instanceId}/message/presence" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "5511999999999",
    "state": "composing"
  }'

Resposta de Sucesso (HTTP 200)

{
  "message": "success",
  "data": {
    "timestamp": "2026-04-24 23:30:00.000"
  }
}

Mapeamento de Erros

| Erro | Status HTTP | Mensagem | Causa | Solução | |-------|-------------|----------|--------|----------| | Número obrigatório | 400 | "phone number is required" | Campo number não enviado | Incluir o número de telefone | | Estado obrigatório | 400 | "state is required" | Campo state não enviado | Informar composing ou paused | | Número inválido | 500 | "invalid phone number" | Formato de número inválido | Usar formato DDI+DDD+Número | | Instância não encontrada | 500 | "instance not found" | Instance ID inválido | Verificar o ID da instância | | Instância desconectada | 500 | "client disconnected" | Instância não conectada ao WhatsApp | Conectar a instância primeiro | | Sessão não ativa | 500 | "no active session found" | Não há sessão ativa | Escanear QR code novamente |


Método 2: Automático via campo delay

Todos os endpoints de envio (/send/text, /send/media, /send/link, /send/button, /send/list, etc.) possuem o campo delay que simula automaticamente a digitação antes de enviar a mensagem.

Como funciona

Quando delay é informado com valor > 0:

  1. O sistema envia o estado "composing" para o destinatário
  2. Aguarda o tempo especificado em milissegundos
  3. Envia a mensagem
  4. O estado "paused" é enviado automaticamente

Para envio de áudio (/send/media com type: "audio"), o sistema usa o estado "composing" com mídia "audio", exibindo "gravando áudio..." em vez de "digitando...".

Campos com delay

| Endpoint | Campo | Tipo | Descrição | |----------|-------|------|-----------| | /send/text | delay | integer | Tempo em ms antes de enviar | | /send/media | delay | integer | Tempo em ms antes de enviar | | /send/link | delay | integer | Tempo em ms antes de enviar | | /send/button | delay | integer | Tempo em ms antes de enviar | | /send/list | delay | integer | Tempo em ms antes de enviar | | /send/carousel | delay | integer | Tempo em ms antes de enviar |


Exemplo: Enviar texto com simulação de digitação

POST /send/text
Content-Type: application/json
{
  "number": "5511999999999",
  "text": "Olá! Seu pedido foi confirmado.",
  "delay": 3000
}

Comportamento:

  1. Exibe "digitando..." para o destinatário
  2. Aguarda 3 segundos (3000ms)
  3. Envia a mensagem
  4. Remove o indicador de digitando

Exemplo: Enviar áudio com simulação de gravação

POST /send/media
Content-Type: application/json
{
  "number": "5511999999999",
  "url": "https://exemplo.com/audio.ogg",
  "type": "audio",
  "delay": 5000
}

Comportamento:

  1. Exibe "gravando áudio..." para o destinatário
  2. Aguarda 5 segundos (5000ms)
  3. Envia o áudio
  4. Remove o indicador de gravação

Comparação com Pilot Status

Pilot Status:

  • Não possui endpoint dedicado para simulação de digitando
  • Não possui campo delay nos endpoints de envio
  • O indicador "digitando..." aparece apenas durante o processamento real da requisição

Evolution GO:

  • Endpoint dedicado para controle manual de presença
  • Campo delay em todos os endpoints de envio para simulação automática
  • Suporte a áudio - pode exibir "gravando áudio..." em vez de "digitando..."
  • Controle total sobre quando iniciar e parar a simulação

Boas Práticas

1. Simulação Natural de Digitação

// Calcula um delay proporcional ao tamanho do texto
function calcularDelayNatural(texto) {
  // Média de digitação humana: ~50ms por caractere
  // Mínimo de 1 segundo, máximo de 8 segundos
  const delayPorCaractere = 50;
  const delay = Math.min(Math.max(texto.length * delayPorCaractere, 1000), 8000);
  return delay;
}

async function enviarComDigitacaoNatural(instanceId, numero, texto) {
  const delay = calcularDelayNatural(texto);

  return await enviarTexto(instanceId, {
    number: numero,
    text: texto,
    delay: delay
  });
}

// Uso
await enviarComDigitacaoNatural('instance-uuid', '5511999999999',
  'Olá! Seu pedido foi confirmado e será entregue em 30 minutos.'
);
// Delay calculado: ~3300ms (66 caracteres * 50ms)

2. Fluxo Completo com Digitando Manual

async function enviarComFluxoDigitacao(instanceId, numero, texto) {
  // 1. Iniciar digitando
  await fetch(
    `http://localhost:3000/instance/${instanceId}/message/presence`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ number: numero, state: 'composing' })
    }
  );

  // 2. Simular tempo de digitação
  const delay = calcularDelayNatural(texto);
  await new Promise(resolve => setTimeout(resolve, delay));

  // 3. Enviar a mensagem
  const result = await enviarTexto(instanceId, {
    number: numero,
    text: texto
  });

  // 4. Parar digitando (normalmente automático após envio)
  await fetch(
    `http://localhost:3000/instance/${instanceId}/message/presence`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ number: numero, state: 'paused' })
    }
  );

  return result;
}

3. Simulação de Gravação de Áudio

async function enviarAudioComGravacao(instanceId, numero, audioUrl) {
  // Calcula delay baseado no tamanho do arquivo (exemplo)
  const delay = 3000; // 3 segundos simulando gravação

  return await enviarMidia(instanceId, {
    number: numero,
    url: audioUrl,
    type: 'audio',
    delay: delay
  });
}

// Uso - o destinatário verá "gravando áudio..." por 3 segundos
await enviarAudioComGravacao(
  'instance-uuid',
  '5511999999999',
  'https://exemplo.com/audio-resposta.ogg'
);

4. Digitando Antes de Enviar Mídia

async function enviarMidiaComDigitacao(instanceId, numero, midia, tipo) {
  // Simular digitando por 2 segundos antes de enviar imagem/vídeo/documento
  const delay = 2000;

  return await enviarMidia(instanceId, {
    number: numero,
    url: midia.url,
    type: tipo, // 'image', 'video', 'document'
    caption: midia.caption,
    delay: delay
  });
}

// Uso
await enviarMidiaComDigitacao('instance-uuid', '5511999999999', {
  url: 'https://exemplo.com/foto-produto.jpg',
  caption: 'Confira nosso novo produto!'
}, 'image');

5. Combinação com Chatbots

class BotComDigitacao {
  constructor(instanceId) {
    this.instanceId = instanceId;
  }

  async responder(numero, texto) {
    // Digitando...
    await this.iniciarDigitacao(numero);

    // Simular tempo de "processamento" do bot
    await new Promise(resolve => setTimeout(resolve, 1500));

    // Enviar com delay natural
    const delay = calcularDelayNatural(texto);
    return await enviarTexto(this.instanceId, {
      number: numero,
      text: texto,
      delay: delay
    });
  }

  async iniciarDigitacao(numero) {
    await fetch(
      `http://localhost:3000/instance/${this.instanceId}/message/presence`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ number: numero, state: 'composing' })
      }
    );
  }

  async pararDigitacao(numero) {
    await fetch(
      `http://localhost:3000/instance/${this.instanceId}/message/presence`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ number: numero, state: 'paused' })
      }
    );
  }
}

// Uso
const bot = new BotComDigitacao('instance-uuid');

// Quando receber uma mensagem via webhook
bot.responder('5511999999999',
  'Olá! Recebi sua mensagem. Estou verificando seu pedido...'
);

Diferenças entre os dois métodos

| Aspecto | Método Manual (/message/presence) | Método Automático (delay) | |----------|--------------------------------------|----------------------------| | Controle | Total (iniciar/parar quando quiser) | Automático (antes do envio) | | Complexidade | Requer chamadas separadas | Apenas um campo no envio | | Casos de uso | Simular atividade antes de processar | Simular digitação antes da mensagem | | Áudio | Suporta isAudio: true | Automático para type: "audio" | | Parada | Manual via state: "paused" | Automático após envio | | Recomendação | Para fluxos complexos de bot | Para envio simples com simulação |

Recomendação geral: Use o campo delay para a maioria dos casos. Use o endpoint manual /message/presence apenas quando precisar de controle total sobre o indicador de digitando (ex: antes de processar dados, consultar API externa, etc.).


Resposta de Sucesso (HTTP 200)

Formato da Resposta

{
  "message": "success",
  "data": {
    "Info": {
      "MessageSource": {
        "Chat": "[email protected]",
        "Sender": "[email protected]",
        "IsFromMe": true,
        "IsGroup": false
      },
      "ID": "3EB0ABCDEF1234567890",
      "Timestamp": 1713993600000,
      "ServerID": 1234567890,
      "Type": "ImageMessage"
    },
    "Message": {
      "imageMessage": {
        "caption": "Olá! Confira esta imagem",
        "url": "https://mmg.whatsapp.net/v/t62.xxxxxx",
        "mediaKey": "abc123...",
        "mimetype": "image/jpeg",
        "fileSha256": "abc123...",
        "fileLength": 12345,
        "height": 1080,
        "width": 1920
      }
    },
    "MessageContextInfo": null
  }
}

Campos da Resposta

| Campo | Descrição | Equivalente Pilot Status | |-------|-----------|------------------------| | data.Info.ID | ID da mensagem no WhatsApp (gerado pelo WhatsApp) | messageId | | data.Info.Timestamp | Timestamp do envio (Unix timestamp) | createdAt (formato ISO) | | data.Info.Type | Tipo da mensagem (ImageMessage, VideoMessage, DocumentMessage) | Tipo da mensagem | | data.Info.ServerID | ID do servidor do WhatsApp | ID interno do servidor | | data.Message.<tipo> | Estrutura da mensagem enviada | Conteúdo da mensagem |


Mapeamento de Erros

Erros de Validação (HTTP 400)

| Erro | Status HTTP | Mensagem | Causa | Solução | |-------|-------------|----------|--------|----------| | Instância não encontrada | 500 | "instance not found" | Instance ID inválido ou não existe | Verificar o ID da instance na URL | | Número obrigatório | 400 | "phone number is required" | Campo number não enviado | Incluir o número no request | | Tipo de mídia obrigatório | 400 | "media type is required" | Campo type não enviado | Especificar o tipo (image, video, document) | | Arquivo obrigatório | 400 | "file is required" | Campo file não enviado (multipart) | Incluir o arquivo no form-data | | URL obrigatória | 400 | "URL is required" | Campo url não enviado (JSON) | Incluir a URL da mídia | | Delay inválido | 400 | "invalid delay" | Campo delay não é um número válido | Usar valor numérico inteiro | | Encoding base64 inválido | 400 | "invalid base64 encoding" | String base64 malformada | Validar o encoding base64 | | Formato de arquivo inválido | 500 | "Invalid file format: 'xxx'. Only 'image/jpeg', 'image/png' and 'image/webp' are accepted" | Formato não suportado | Usar formatos suportados |

Erros de Conexão/Instância (HTTP 500)

| Erro | Mensagem | Causa | Solução | |-------|----------|--------|----------| | Instância desconectada | "client disconnected" | A instance não está conectada ao WhatsApp | 1. Conectar a instance via QR code<br>2. Verificar status da connection<br>3. Aguardar reconexão automática (o sistema tenta reconectar até 3 vezes) | | Sessão não ativa | "no active session found" | Não há sessão ativa para a instance | 1. Verificar se a instance foi criada<br>2. Escanear o QR code novamente<br>3. Aguardar a inicialização completa | | Falha ao iniciar instance | "Failed to start instance" | Erro ao tentar iniciar a instance | 1. Verificar logs do servidor<br>2. Verificar conectividade com WhatsApp<br>3. Tentar deletar e recriar a instance |

Erros de Envio (HTTP 500)

| Erro | Mensagem | Causa | Solução | |-------|----------|--------|----------| | Falha ao enviar | "failed to send text after X attempts" | Erro após múltiplas tentativas de envio | 1. Verificar conexão com internet<br>2. Verificar se o número é válido<br>3. Verificar se a instance está conectada | | Falha ao enviar mídia | "failed to send media url after X attempts" | Erro ao baixar/enviar a mídia | 1. Verificar se a URL é acessível<br>2. Verificar o tamanho do arquivo (max 100MB)<br>3. Verificar formato do arquivo | | Usuário não encontrado | error contendo "user not found" | Número inválido ou não existe no WhatsApp | 1. Verificar formato do número (DDI + DDD + Número)<br>2. Testar enviar mensagem manualmente |


Comportamento de Retry Automático

A Evolution GO implementa retry automático para tentar recuperar de erros de conexão:

Configuração de Retry

| Parâmetro | Valor | Descrição | |-----------|--------|-----------| | Máximo de tentativas | 3 | Número máximo de tentativas de envio | | Tentativas de conexão | 2 | Tentativas para estabelecer conexão com o WhatsApp | | Backoff progressivo | Sim | Tempo de espera aumenta a cada tentativa |

Processo de Retry

  1. Tentativa 1: Envio inicial
  2. Se falhar com erro de conexão:
    • Tenta reconectar a instance
    • Aguarda 1-2 segundos
    • Tentativa 2
  3. Se falhar novamente:
    • Tenta reconectar novamente
    • Aguarda 2-4 segundos (backoff progressivo)
    • Tentativa 3
  4. Se falhar na última tentativa: Retorna erro HTTP 500

Logs de Retry

O sistema gera logs detalhados durante o processo de retry:

[instance-uuid] SendMedia attempt 1/3
[instance-uuid] Connection attempt 1/2
[instance-uuid] Checking client connection status - Client exists: true
[instance-uuid] Client disconnected on attempt 1/3, attempting reconnection...
[instance-uuid] Waiting 2s before retry attempt 2
[instance-uuid] SendMedia attempt 2/3
...

Comparação com Pilot Status

Enviando Imagem

Pilot Status:

POST /v1/messages/send
{
  "templateId": "envio-imagem",
  "destinationNumber": "+5511999999999",
  "variables": {
    "imageUrl": "https://exemplo.com/imagem.jpg",
    "caption": "Olá!"
  }
}

Resposta (HTTP 202):

{
  "id": "cmn0qk33b001kmj01q37f6quv",
  "status": "QUEUED",
  "createdAt": "2026-04-24T15:00:00.000Z"
}

Evolution GO:

POST /instance/{instanceId}/send/media
{
  "number": "5511999999999",
  "url": "https://exemplo.com/imagem.jpg",
  "type": "image",
  "caption": "Olá!",
  "id": "msg-custom-001"
}

Resposta (HTTP 200):

{
  "message": "success",
  "data": {
    "Info": {
      "ID": "3EB0ABCDEF1234567890",
      "Type": "ImageMessage",
      "Timestamp": 1713993600000
    }
  }
}

Diferenças:

  1. Status HTTP: Pilot Status usa 202 (aceito), Evolution GO usa 200 (sucesso)
  2. ID do envio: Pilot Status retorna id interno, Evolution GO retorna ID do WhatsApp
  3. Formato do número: Pilot Status usa +55..., Evolution GO usa 55...
  4. Endpoint: Pilot Status usa /v1/messages/send, Evolution GO usa /instance/{id}/send/media

Boas Práticas

1. Tratamento de Erros

async function enviarMimeEvolution(instanceId, dados) {
  try {
    const response = await fetch(
      `http://localhost:3000/instance/${instanceId}/send/media`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(dados)
      }
    );

    if (!response.ok) {
      const error = await response.json();
      
      if (response.status === 400) {
        // Erro de validação
        console.error('Erro de validação:', error.error);
        throw new Error(`Validação: ${error.error}`);
      } else if (response.status === 500) {
        // Erro de servidor/instância
        if (error.error.includes('disconnected') || 
            error.error.includes('no active session')) {
          console.error('Instância desconectada. Tentando reconectar...');
          throw new Error('Instância desconectada. Verifique a conexão.');
        } else {
          console.error('Erro ao enviar:', error.error);
          throw new Error(`Erro: ${error.error}`);
        }
      }
    }

    const result = await response.json();
    console.log('Mensagem enviada com sucesso:', result.data.Info.ID);
    return result.data;

  } catch (error) {
    console.error('Erro ao enviar mensagem:', error.message);
    throw error;
  }
}

2. Verificação de Status da Instância

async function verificarInstancia(instanceId) {
  try {
    const response = await fetch(
      `http://localhost:3000/instance/${instanceId}/status`
    );
    
    if (!response.ok) {
      throw new Error('Instância não encontrada');
    }

    const status = await response.json();
    
    if (!status.state === 'open') {
      console.warn('Instância não está conectada');
      return false;
    }
    
    return true;

  } catch (error) {
    console.error('Erro ao verificar instância:', error.message);
    return false;
  }
}

// Uso
const conectada = await verificarInstancia('instance-uuid');
if (conectada) {
  await enviarMimeEvolution('instance-uuid', dados);
} else {
  console.error('Não foi possível enviar: instância desconectada');
}

3. Uso de ID Customizado

// Ao enviar
const mensagemEnviada = await enviarMimeEvolution('instance-uuid', {
  number: '5511999999999',
  url: 'https://exemplo.com/imagem.jpg',
  type: 'image',
  id: 'msg-empresa-12345'  // ID customizado
});

// Guarde no seu banco de dados
await database.mensagens.create({
  id_interno: 'msg-empresa-12345',
  id_whatsapp: mensagemEnviada.Info.ID,
  numero: '5511999999999',
  timestamp: new Date(),
  status: 'enviada'
});

// Quando receber webhook Message com FromMe: true
// data.ID == mensagemEnviada.Info.ID

4. Retry Manual (se necessário)

async function enviarComRetryManual(dados, maxTentativas = 3) {
  for (let tentativa = 1; tentativa <= maxTentativas; tentativa++) {
    try {
      const resultado = await enviarMimeEvolution('instance-uuid', dados);
      console.log(`Sucesso na tentativa ${tentativa}`);
      return resultado;
    } catch (error) {
      if (tentativa === maxTentativas) {
        throw new Error(`Falha após ${maxTentativas} tentativas: ${error.message}`);
      }
      console.warn(`Tentativa ${tentativa} falhou. Aguardando...`);
      await new Promise(resolve => setTimeout(resolve, 2000 * tentativa));
    }
  }
}

Endpoint: POST /send/link (Link Preview)

Descrição: Envia mensagem de texto com link preview automático. O sistema busca automaticamente os metadados da URL (título, descrição e imagem) para gerar um preview rico.

Características:

  • Busca automática de metadados da URL (Open Graph tags)
  • Preview rico com título, descrição e thumbnail
  • Detecção automática de URL no texto
  • Download automático da imagem de preview
  • Retry automático em caso de falha

Estrutura da Requisição

Campos Obrigatórios

| Campo | Tipo | Descrição | |-------|------|-----------| | number | string | Número de telefone (com DDI, ex: 5511999999999) | | text | string | Texto da mensagem (deve conter uma URL) |

Campos Opcionais

| Campo | Tipo | Descrição | |-------|------|-----------| | url | string | URL para o preview (se não informado, busca automaticamente no text) | | title | string | Título personalizado do preview (sobrescreve o automático) | | description | string | Descrição personalizada do preview (sobrescreve a automática) | | imgUrl | string | URL da imagem personalizada (sobrescreve a automática) | | id | string | ID customizado da mensagem | | delay | integer | Delay em milissegundos antes do envio | | mentionedJid | string[] | Lista de JIDs para mencionar | | mentionAll | boolean | true para mencionar todos | | formatJid | boolean | Formatar JID para visualização | | quoted.messageId | string | ID da mensagem para responder (reply-to) | | quoted.participant | string | Participante da mensagem citada (para grupos) |


Exemplos de Uso

Exemplo 1: Link com Preview Automático

POST /send/link
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999",
  "text": "Confira este artigo incrível: https://exemplo.com/artigo-interessante",
  "id": "link-msg-001"
}

Resultado:

  • O sistema detecta a URL automaticamente no texto
  • Busca metadados da página (title, og:description, og:image)
  • Faz download da imagem de preview
  • Envia mensagem com preview rico

Exemplo 2: Link com Preview Personalizado

POST /send/link
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999",
  "url": "https://exemplo.com/artigo",
  "title": "Artigo Completo Sobre Tecnologia",
  "description": "Descubra as últimas tendências em tecnologia e inovação.",
  "imgUrl": "https://exemplo.com/imagens/preview-tech.jpg",
  "text": "Acabamos de publicar um novo artigo! Confira: https://exemplo.com/artigo",
  "id": "link-msg-002"
}

Exemplo 3: Link com Múltiplas URLs

POST /send/link
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999",
  "text": "Veja estes links interessantes:\n1. https://site1.com\n2. https://site2.com",
  "url": "https://site1.com",
  "id": "link-msg-003"
}

Nota: Quando há múltiplas URLs, o campo url define qual será usada para o preview.

Exemplo com cURL

curl -X POST "http://localhost:3000/instance/{instanceId}/send/link" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "5511999999999",
    "text": "Confira nosso novo produto: https://loja.com/produto",
    "id": "promo-001"
  }'

Resposta de Sucesso (HTTP 200)

{
  "message": "success",
  "data": {
    "Info": {
      "MessageSource": {
        "Chat": "[email protected]",
        "Sender": "[email protected]",
        "IsFromMe": true,
        "IsGroup": false
      },
      "ID": "3EB0ABCDEF1234567890",
      "Timestamp": 1713993600000,
      "ServerID": 1234567890,
      "Type": "ExtendedTextMessage"
    },
    "Message": {
      "extendedTextMessage": {
        "text": "Confira nosso novo produto: https://loja.com/produto",
        "title": "Produto Incrível - Oferta Especial",
        "description": "O melhor produto com desconto exclusivo!",
        "matchedText": "https://loja.com/produto",
        "JPEGThumbnail": "base64-encoded-thumbnail...",
        "previewType": 0
      }
    },
    "MessageContextInfo": null
  }
}

Campos da Resposta

| Campo | Descrição | |-------|-----------| | data.Info.ID | ID da mensagem no WhatsApp | | data.Info.Type | Tipo da mensagem (ExtendedTextMessage) | | data.Message.extendedTextMessage.text | Texto completo da mensagem | | data.Message.extendedTextMessage.title | Título do preview (automático ou personalizado) | | data.Message.extendedTextMessage.description | Descrição do preview | | data.Message.extendedTextMessage.matchedText | URL que foi detectada para o preview | | data.Message.extendedTextMessage.JPEGThumbnail | Imagem de preview em base64 | | data.Message.extendedTextMessage.previewType | Tipo de preview (0 = vídeo/outros) |


Funcionamento do Fetch de Metadados

O sistema realiza as seguintes operações automaticamente:

  1. Detecção de URL: Procura URLs no campo text ou usa o campo url
  2. Download da página: Faz HTTP GET para a URL
  3. Parse HTML: Extrai metadados das tags Open Graph:
    • <title> ou og:title
    • <meta name="description"> ou og:description
    • <meta property="og:image">
  4. Download da imagem: Se encontrou og:image, faz download da imagem
  5. Construção do preview: Monta o ExtendedTextMessage com os metadados
  6. Envio: Envia a mensagem com preview rico

Tags HTML Suportadas

<!-- Título -->
<title>Título da Página</title>
<meta property="og:title" content="Título do Site">

<!-- Descrição -->
<meta name="description" content="Descrição do site">
<meta property="og:description" content="Descrição Open Graph">

<!-- Imagem -->
<meta property="og:image" content="https://exemplo.com/imagem.jpg">

Mapeamento de Erros

Erros de Validação (HTTP 400)

| Erro | Mensagem | Causa | Solução | |-------|----------|--------|---------| | Número obrigatório | "phone number is required" | Campo number não enviado | Incluir o número | | Texto obrigatório | "message body is required" | Campo text não enviado | Incluir o texto | | URL inválida | Erro ao baixar a página | URL não existe ou retorna erro | 1. Verificar se a URL está correta<br>2. Testar acessar a URL no browser<br>3. Usar URL personalizada se necessário | | Falha ao baixar imagem | Erro ao baixar og:image | Imagem de preview não disponível | 1. Fornecer imgUrl personalizado<br>2. Verificar se a imagem existe<br>3. Usar sem preview |

Erros de Conexão/Instância (HTTP 500)

| Erro | Mensagem | Causa | Solução | |-------|----------|--------|---------| | Instância desconectada | "client disconnected" | Instance não está conectada | 1. Conectar via QR code<br>2. Verificar status da conexão | | Sessão não ativa | "no active session found" | Não há sessão ativa | 1. Escanear QR code novamente<br>2. Aguardar inicialização |

Erros de Envio (HTTP 500)

| Erro | Mensagem | Causa | Solução | |-------|----------|--------|---------| | Falha ao enviar link | "failed to send link after X attempts" | Erro após múltiplas tentativas | 1. Verificar conexão com internet<br>2. Verificar se a URL é acessível<br>3. Verificar se a instance está conectada | | Falha no fetch de metadados | Erro ao baixar página HTML | Página não responde ou bloqueia bots | 1. Usar metadados personalizados<br>2. Verificar se a URL permite web scraping<br>3. Usar User-Agent personalizado se necessário |


Comparação com Pilot Status

Pilot Status

A Pilot Status não possui um endpoint específico para link preview. Para enviar uma mensagem com preview:

POST /v1/messages/send
{
  "templateId": "envio-texto",
  "destinationNumber": "+5511999999999",
  "variables": {
    "texto": "Confira: https://exemplo.com"
  }
}

Limitações:

  • ❌ Não há busca automática de metadados
  • ❌ Preview depende do WhatsApp (não controlado)
  • ❌ Não é possível personalizar título/descrição

Evolution GO

POST /instance/{instanceId}/send/link
{
  "number": "5511999999999",
  "text": "Confira nosso novo artigo!",
  "url": "https://exemplo.com/artigo",
  "title": "Artigo Completo",
  "description": "Descubra as últimas novidades",
  "imgUrl": "https://exemplo.com/preview.jpg",
  "id": "link-001"
}

Vantagens:

  • Busca automática de metadados (title, description, image)
  • Personalização total do preview
  • Controle absoluto sobre o que é exibido
  • Preview garantido mesmo que a página não tenha metadados
  • Retry automático em caso de falha no fetch

Boas Práticas

1. Tratamento de Erros no Fetch de Metadados

async function enviarLinkComPreview(instanceId, dados) {
  try {
    const response = await fetch(
      `http://localhost:3000/instance/${instanceId}/send/link`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(dados)
      }
    );

    if (!response.ok) {
      const error = await response.json();
      
      // Tratar erros específicos
      if (error.error.includes('failed to fetch link metadata')) {
        console.warn('Não foi possível buscar metadados. Tentando sem preview...');
        
        // Retentar sem preview (usar /send/text)
        const textoFallback = dados.url ? dados.text.replace(dados.url, '') : dados.text;
        return await enviarTexto(instanceId, {
          number: dados.number,
          text: textoFallback
        });
      }
      
      throw new Error(`Erro: ${error.error}`);
    }

    const result = await response.json();
    console.log('Link enviado com preview:', result.data.Info.ID);
    return result.data;

  } catch (error) {
    console.error('Erro ao enviar link:', error.message);
    throw error;
  }
}

2. Fornecer Fallback para Metadados

async function enviarLinkComFallback(instanceId, url, texto) {
  // Tenta buscar metadados automaticamente
  let metadados = {};
  
  try {
    metadados = await buscarMetadadosOpenGraph(url);
  } catch (error) {
    console.warn('Falha ao buscar metadados, usando fallback:', error.message);
    
    // Fallback com metadados genéricos
    metadados = {
      title: 'Confira este link',
      description: 'Clique para acessar o conteúdo',
      imgUrl: 'https://exemplo.com/imagens/link-padrao.jpg'
    };
  }

  return await enviarLink(instanceId, {
    number: '5511999999999',
    text: texto,
    url: url,
    ...metadados,
    id: 'link-fallback-001'
  });
}

function buscarMetadadosOpenGraph(url) {
  // Implementação customizada se precisar de controle mais fino
  return {
    title: 'Título Encontrado',
    description: 'Descrição Encontrada',
    imgUrl: 'https://site.com/imagem.jpg'
  };
}

3. Uso de Preview Personalizado

async function enviarPromocaoComPreview(instanceId, produto) {
  const dados = {
    number: produto.numero,
    text: `${produto.nome}\n${produto.descricao}\n${produto.url}`,
    url: produto.url,
    title: produto.nome,  // Nome do produto como título
    description: produto.descricao.substring(0, 300), // Descrição truncada
    imgUrl: produto.imagem_preview, // Imagem profissional do produto
    id: `promo-${produto.id}`
  };

  return await enviarLink(instanceId, dados);
}

// Uso
await enviarPromocaoComPreview('instance-uuid', {
  numero: '5511999999999',
  nome: 'iPhone 15 Pro',
  descricao: 'O smartphone mais potente da Apple com chip A17 Pro...',
  url: 'https://loja.com/iphone-15-pro',
  imagem_preview: 'https://loja.com/imagens/iphone-15-pro-banner.jpg'
});

4. Tratamento de Múltiplas URLs

async function enviarComMultiplasUrls(instanceId, numero, urls) {
  // Envia apenas a primeira URL com preview
  const primeiraUrl = urls[0];  
  const texto = `📰 Veja estas notícias:\n\n` + 
    urls.map((url, index) => `${index + 1}. ${url}`).join('\n');

  const dados = {
    number: numero,
    text: texto,
    url: primeiraUrl, // Preview da primeira URL
    id: 'multi-urls-001'
  };

  return await enviarLink(instanceId, dados);
}

// Uso
await enviarComMultiplasUrls('instance-uuid', '5511999999999', [
  'https://noticias.com/artigo1',
  'https://noticias.com/artigo2',
  'https://noticias.com/artigo3'
]);

5. Verificação de Suporte a Preview

async function verificarSuportePreview(url) {
  try {
    const response = await fetch(url, {
      method: 'HEAD',
      headers: { 'User-Agent': 'Mozilla/5.0' }
    });

    const contentType = response.headers.get('content-type');
    const isHtml = contentType?.includes('text/html');

    if (!isHtml) {
      console.warn('URL não é HTML, preview pode não funcionar:', url);
      return false;
    }

    return true;

  } catch (error) {
    console.error('Erro ao verificar URL:', error.message);
    return false;
  }
}

// Uso antes de enviar
const temPreview = await verificarSuportePreview('https://exemplo.com/artigo');
if (temPreview) {
  await enviarLink(instanceId, { number, text, url });
} else {
  await enviarTexto(instanceId, { number, text });
}

Fluxo Completo de Envio de Link

1. Cliente envia requisição para /send/link
   ↓
2. Evolution GO detecta URL no texto ou usa campo url
   ↓
3. Sistema faz HTTP GET para a página
   ↓
4. Parse HTML e extrai metadados:
   - <title> ou og:title
   - <meta name="description"> ou og:description
   - og:image
   ↓
5. Download da imagem de preview (og:image)
   ↓
6. Constrói ExtendedTextMessage com:
   - Texto completo
   - Título
   - Descrição
   - Imagem (base64)
   - Preview type
   ↓
7. Envia mensagem para o WhatsApp
   ↓
8. Cliente recebe resposta com ID da mensagem
   ↓
9. Cliente pode correlacionar com webhooks subsequentes

Diferenças entre /send/text e /send/link

| Aspecto | /send/text | /send/link | |----------|-------------|-------------| | Preview automático | ❌ Não | ✅ Sim | | Busca de metadados | ❌ Não | ✅ Sim | | Thumbnail personalizada | ❌ Não | ✅ Sim | | Título/descrição | ❌ Não | ✅ Sim | | Controle do preview | ❌ Depende do WhatsApp | ✅ Completo | | Caso de uso | Mensagens simples | Links com preview rico |

Recomendação: Use /send/link sempre que enviar mensagens contendo URLs para garantir um preview profissional e controlado.


Endpoint de Gestão de Instância

A Evolution GO fornece endpoints completos para criar instâncias, gerar QR codes, obter códigos de pareamento e configurar proxy.


1. Criar Nova Instância (POST /instance/create)

Descrição: Cria uma nova instância (conexão do WhatsApp) que já inicia o processo de geração do QR code.

Endpoint:

POST /instance/create
Content-Type: application/json

Campos Obrigatórios

| Campo | Tipo | Descrição | |-------|------|-----------| | instanceId | string | Identificador único da instância (UUID) | | name | string | Nome da instância (ex: "Atendimento Vendas") | | token | string | Token de autenticação da API (deve ser único) |

Campos Opcionais

| Campo | Tipo | Descrição | |-------|------|-----------| | proxy | object | Configuração de proxy para a instância | | advancedSettings | object | Configurações avançadas (rejeitar chamadas, etc.) |


Configuração de Proxy

Estrutura do objeto proxy:

{
  "protocol": "http",
  "host": "proxy.exemplo.com",
  "port": "8080",
  "username": "usuario",
  "password": "senha"
}

| Campo | Tipo | Descrição | |-------|------|-----------| | protocol | string | Protocolo do proxy: http ou socks5 | | host | string | Endereço do servidor proxy | | port | string | Porta do proxy | | username | string | Usuário do proxy (se exigido) | | password | string | Senha do proxy (se exigida) |


Configurações Avançadas

Estrutura do objeto advancedSettings:

{
  "alwaysOnline": false,
  "rejectCall": false,
  "msgRejectCall": "No momento não atendemos chamadas.",
  "readMessages": false,
  "ignoreGroups": false,
  "ignoreStatus": false
}

| Campo | Tipo | Padrão | Descrição | |-------|------|---------|-----------| | alwaysOnline | boolean | false | Mantém a instância sempre online | | rejectCall | boolean | false | Rejeita chamadas automaticamente | | msgRejectCall | string | "" | Mensagem enviada ao rejeitar chamada | | readMessages | boolean | false | Marca mensagens como lidas automaticamente | | ignoreGroups | boolean | false | Ignora mensagens de grupos | | ignoreStatus | boolean | false | Ignora mensagens de status/broadcast |


Exemplo 1: Criação Básica

POST /instance/create
Content-Type: application/json

Corpo da requisição:

{
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Atendimento Comercial",
  "token": "api-token-12345"
}

Exemplo 2: Criação com Proxy

POST /instance/create
Content-Type: application/json

Corpo da requisição:

{
  "instanceId": "550e8400-e29b-41d4-a716-446655440001",
  "name": "Atendimento Vendas",
  "token": "api-token-67890",
  "proxy": {
    "protocol": "http",
    "host": "proxy.empresa.com.br",
    "port": "3128",
    "username": "empresa_user",
    "password": "senha_segura"
  }
}

Exemplo 3: Criação com Configurações Avançadas

POST /instance/create
Content-Type: application/json

Corpo da requisição:

{
  "instanceId": "550e8400-e29b-41d4-a716-446655440002",
  "name": "Bot de Suporte",
  "token": "api-token-suporte",
  "advancedSettings": {
    "alwaysOnline": true,
    "rejectCall": true,
    "msgRejectCall": "Este número não atende chamadas. Use o chat para contato.",
    "readMessages": true,
    "ignoreGroups": true
  }
}

Exemplo com cURL

curl -X POST "http://localhost:3000/instance/create" \
  -H "Content-Type: application/json" \
  -d '{
    "instanceId": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Atendimento",
    "token": "api-token-12345",
    "proxy": {
      "protocol": "http",
      "host": "proxy.exemplo.com",
      "port": "8080"
    }
  }'

Resposta de Sucesso (HTTP 200)

{
  "message": "success",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Atendimento Comercial",
    "token": "api-token-12345",
    "webhook": "",
    "rabbitmqEnable": "",
    "websocketEnable": "",
    "natsEnable": "",
    "jid": "",
    "qrcode": "",
    "connected": false,
    "expiration": 0,
    "disconnect_reason": "",
    "events": "",
    "os_name": "linux",
    "proxy": "{\"protocol\":\"http\",\"host\":\"proxy.exemplo.com\",\"port\":\"8080\"}",
    "client_name": "EvolutionAPI",
    "createdAt": "2026-04-24T23:00:00.000Z",
    "alwaysOnline": false,
    "rejectCall": false,
    "msgRejectCall": "",
    "readMessages": false,
    "ignoreGroups": false,
    "ignoreStatus": false
  }
}

Mapeamento de Erros

| Erro | Status HTTP | Mensagem | Causa | Solução | |-------|-------------|----------|--------|----------| | Nome obrigatório | 400 | "name is required" | Campo name não enviado | Incluir o nome da instância | | Token obrigatório | 400 | "token is required" | Campo token não enviado | Incluir o token da API | | Porta do proxy obrigatória | 400 | "proxy port is required" | Proxy informado sem porta | Informar a porta do proxy | | Senha do proxy obrigatória | 400 | "proxy password is required" | Proxy com autenticação sem senha | Informar a senha do proxy | | Usuário do proxy obrigatório | 400 | "proxy username is required" | Proxy com autenticação sem usuário | Informar o usuário do proxy | | Instância já existe | 500 | "instance already exists" | Já existe instância com o mesmo nome | Usar um nome diferente ou excluir a existente | | Erro ao criar instância | 500 | Erro genérico | Erro interno no banco de dados | Verificar logs do servidor |


2. Obter QR Code (GET /instance/{instanceId}/qr)

Descrição: Obtém o QR code atual da instância. Se não houver QR code disponível, o sistema inicia automaticamente uma nova instância e gera um QR code. Este endpoint também serve para regenerar o QR code quando o anterior expirou ou não foi usado.

Endpoint:

GET /instance/{instanceId}/qr

Autenticação: Requer header apikey com o token da instância.


Comportamento Interno (Geração/Regeneração)

Ao chamar este endpoint, o sistema executa automaticamente:

  1. Verifica se há cliente ativo para a instância
  2. Se não há cliente ou está logado:
    • Inicia uma nova instância do WhatsApp
    • Aguarda 3 segundos para o QR code ser gerado
    • Verifica se a sessão não está logada
  3. Se o cliente existe mas não está conectado:
    • Verifica se já existe QR code gerado
  4. Busca o QR code no banco de dados
  5. Se não há QR code ainda:
    • Aguarda 2 segundos adicionais
    • Tenta buscar novamente
  6. Retorna o QR code em formato base64 + código

Importante: Cada chamada a este endpoint pode gerar um novo QR code se o anterior expirou (QR codes do WhatsApp expiram em ~60 segundos).


Exemplo de Uso

GET /instance/550e8400-e29b-41d4-a716-446655440000/qr
apikey: api-token-12345

Exemplo com cURL

curl -X GET "http://localhost:3000/instance/550e8400-e29b-41d4-a716-446655440000/qr" \
  -H "apikey: api-token-12345"

Resposta de Sucesso (HTTP 200)

{
  "message": "success",
  "data": {
    "qrcode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAA...",
    "code": "3@7D8J7-3H7L9-K6M3-N5P2-R8S4-V9T2-W8Y3"
  }
}

| Campo | Descrição | |-------|-----------| | data.qrcode | QR code completo em formato base64 (data:image/png;base64,...) | | data.code | String do QR code (texto que pode ser codificado de outra forma) |


Como Usar o QR Code

1. Exibir a Imagem Base64:

const qrData = response.data.qrcode;

// Criar elemento de imagem
const img = document.createElement('img');
img.src = qrData;
document.body.appendChild(img);

2. Decodificar em Canvas:

const qrData = response.data.qrcode.split(',')[1]; // Remove "data:image/png;base64,"
const binaryString = atob(qrData);
const bytes = new Uint8Array(binaryString.length);

for (let i = 0; i < binaryString.length; i++) {
  bytes[i] = binaryString.charCodeAt(i);
}

const blob = new Blob([bytes], { type: 'image/png' });
const url = URL.createObjectURL(blob);

// Usar a URL para exibir
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);

3. Usar Biblioteca de QR Code:

const code = response.data.code; // String do QR code

// Usar com qrcode.js ou similar
QRCode.toCanvas(code, { width: 300 }, function(error, canvas) {
  if (error) {
    console.error(error);
    return;
  }
  document.body.appendChild(canvas);
});

Mapeamento de Erros

| Erro | Status HTTP | Mensagem | Causa | Solução | |-------|-------------|----------|--------|----------| | Instância não encontrada | 404 | "instance not found" | Instance ID não existe ou token inválido | Verificar o ID e token da instância | | Sessão já logada | 400 | "session already logged in" | Já está conectado ao WhatsApp | Não há ação necessária, já está conectado | | Falha ao iniciar instância | 500 | "failed to start instance: ..." | Erro ao iniciar instância | Verificar logs, tentar novamente | | Nenhum QR code disponível | 500 | "no QR code available. Please wait a moment and try again" | Sistema demorou para gerar o QR code | Aguardar alguns segundos e tentar novamente | | Formato de QR inválido | 500 | "invalid QR code format" | QR code no banco está corrompido | Chamar reconexão e tentar novamente |


3. Obter Código de Pareamento (POST /instance/{instanceId}/pair)

Descrição: Gera um código de pareamento de 8 dígitos que pode ser usado para conectar ao WhatsApp sem escanear QR code. O usuário digita o código diretamente no celular. Cada chamada gera um novo código (códigos anteriores são invalidados).

Endpoint:

POST /instance/{instanceId}/pair
Content-Type: application/json

Autenticação: Requer header apikey com o token da instância.

Requisito: A instância deve estar criada e com o cliente WhatsApp iniciado (chamar /instance/connect antes ou aguardar após /instance/create).


Campos Obrigatórios

| Campo | Tipo | Descrição | |-------|------|-----------| | phone | string | Número de telefone para pareamento (com DDI, ex: 5511999999999) |


Como Funciona o Pareamento por Código

  1. O sistema chama a API do WhatsApp para gerar um código vinculado ao número
  2. O código tem formato XXXX-XXXX (8 caracteres alfanuméricos)
  3. O código é exibido para o usuário
  4. No celular, o usuário vai em Aparelhos Conectados → Conectar usando número de telefone
  5. O usuário digita o código no WhatsApp
  6. A conexão é estabelecida automaticamente

Exemplo de Uso

POST /instance/550e8400-e29b-41d4-a716-446655440000/pair
apikey: api-token-12345
Content-Type: application/json

Corpo da requisição:

{
  "phone": "5511999999999"
}

Exemplo com cURL

curl -X POST "http://localhost:3000/instance/550e8400-e29b-41d4-a716-446655440000/pair" \
  -H "apikey: api-token-12345" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "5511999999999"
  }'

Resposta de Sucesso (HTTP 200)

{
  "message": "success",
  "data": {
    "pairingCode": "J7K3-N5P2"
  }
}

| Campo | Descrição | |-------|-----------| | data.pairingCode | Código de pareamento de 8 dígitos (formato XXXX-XXXX) |


Como Usar o Código de Pareamento

No WhatsApp Mobile:

  1. Abra o WhatsApp
  2. Vá em MenuAparelhos Conectados
  3. Toque em Conectar um aparelho
  4. Toque em Conectar usando número de telefone
  5. Digite o código recebido: J7K3-N5P2
  6. Aguarde a conexão

No WhatsApp Web: O código de pareamento não funciona no WhatsApp Web, apenas no aplicativo móvel.


Mapeamento de Erros

| Erro | Status HTTP | Mensagem | Causa | Solução | |-------|-------------|----------|--------|----------| | Instância não encontrada | 500 | "instance not found" | Instance ID não existe ou token inválido | Verificar o ID e token da instância | | Telefone obrigatório | 400 | "phone is required" | Campo phone não enviado | Incluir o número de telefone | | Erro ao gerar código | 500 | "something went wrong calling pair phone" | Erro no processo de pareamento | Verificar o número, tentar novamente | | Cliente não inicializado | 500 | Erro de ponteiro nulo | Instância não foi conectada ainda | Chamar /instance/connect antes |


Gerar Novo QR Code / Pairing Code

Para regenerar um QR code ou pairing code (quando expirou, não funcionou ou precisa de um novo), siga os fluxos abaixo:


Fluxo 1: Regenerar QR Code

Cenário: QR code expirou ou não foi escaneado a tempo.

Método A - Chamar o endpoint diretamente:

# Cada chamada pode gerar um novo QR code automaticamente
curl -X GET "http://localhost:3000/instance/{instanceId}/qr" \
  -H "apikey: api-token-12345"

Método B - Reconectar e gerar novo QR:

# 1. Reconectar a instância (limpa sessão e gera novo QR)
curl -X POST "http://localhost:3000/instance/reconnect" \
  -H "apikey: api-token-12345"

# 2. Aguardar 5 segundos
sleep 5

# 3. Buscar o novo QR code
curl -X GET "http://localhost:3000/instance/{instanceId}/qr" \
  -H "apikey: api-token-12345"

Fluxo 2: Gerar Novo Pairing Code

Cenário: Código de pareamento expirou ou não foi digitado a tempo.

# Cada chamada gera um novo código (o anterior é invalidado)
curl -X POST "http://localhost:3000/instance/{instanceId}/pair" \
  -H "apikey: api-token-12345" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "5511999999999"
  }'

Fluxo 3: Regenerar Tudo (QR Code + Pairing Code)

Cenário: Instância com problemas de conexão, precisa reiniciar do zero.

# 1. Reconectar para limpar sessão
curl -X POST "http://localhost:3000/instance/reconnect" \
  -H "apikey: api-token-12345"

# 2. Aguardar inicialização
sleep 5

# 3. Buscar novo QR code
curl -X GET "http://localhost:3000/instance/{instanceId}/qr" \
  -H "apikey: api-token-12345"

# OU: Gerar novo pairing code
curl -X POST "http://localhost:3000/instance/{instanceId}/pair" \
  -H "apikey: api-token-12345" \
  -H "Content-Type: application/json" \
  -d '{"phone": "5511999999999"}'

Exemplo Completo: Interface de Regeneração

class GerenciadorConexao {
  constructor(instanceId, apiToken) {
    this.instanceId = instanceId;
    this.apiToken = apiToken;
    this.baseUrl = 'http://localhost:3000';
  }

  async obterQrCode() {
    const response = await fetch(
      `${this.baseUrl}/instance/${this.instanceId}/qr`,
      { headers: { 'apikey': this.apiToken } }
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error);
    }

    const result = await response.json();
    return result.data;
  }

  async gerarPairingCode(phone) {
    const response = await fetch(
      `${this.baseUrl}/instance/${this.instanceId}/pair`,
      {
        method: 'POST',
        headers: {
          'apikey': this.apiToken,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ phone })
      }
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error);
    }

    const result = await response.json();
    return result.data.pairingCode;
  }

  async reconectar() {
    const response = await fetch(
      `${this.baseUrl}/instance/reconnect`,
      {
        method: 'POST',
        headers: { 'apikey': this.apiToken }
      }
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error);
    }

    return true;
  }

  async verificarStatus() {
    const response = await fetch(
      `${this.baseUrl}/instance/status`,
      { headers: { 'apikey': this.apiToken } }
    );

    const result = await response.json();
    return result.data.state;
  }

  async aguardarConexao(maxTentativas = 30) {
    for (let i = 0; i < maxTentativas; i++) {
      const status = await this.verificarStatus();
      if (status === 'open') return true;
      await new Promise(resolve => setTimeout(resolve, 2000));
    }
    return false;
  }

  // Fluxo completo: reconectar → gerar QR/pairing → aguardar conexão
  async iniciarConexao(phone = null) {
    // 1. Verificar se já está conectado
    const status = await this.verificarStatus();
    if (status === 'open') {
      console.log('Instância já está conectada!');
      return { connected: true };
    }

    // 2. Reconectar para gerar nova sessão
    console.log('Reconectando instância...');
    await this.reconectar();
    await new Promise(resolve => setTimeout(resolve, 5000));

    // 3. Gerar QR code ou pairing code
    let resultado = {};

    if (phone) {
      // Opção A: Pareamento por código
      console.log('Gerando código de pareamento...');
      resultado.pairingCode = await this.gerarPairingCode(phone);
      console.log(`Código: ${resultado.pairingCode}`);
      console.log('Digite no WhatsApp: Menu → Aparelhos Conectados → Conectar usando número');
    } else {
      // Opção B: QR Code
      console.log('Gerando QR code...');
      const qr = await this.obterQrCode();
      resultado.qrcode = qr.qrcode;
      resultado.code = qr.code;
      console.log('Escaneie o QR code no WhatsApp');
    }

    // 4. Aguardar conexão
    console.log('Aguardando conexão...');
    const conectado = await this.aguardarConexao();

    if (conectado) {
      console.log('Conectado com sucesso!');
      return { connected: true, ...resultado };
    } else {
      console.error('Timeout: não foi possível conectar');
      return { connected: false, ...resultado };
    }
  }
}

// Uso
const gerenciador = new GerenciadorConexao(
  '550e8400-e29b-41d4-a716-446655440000',
  'api-token-12345'
);

// Opção 1: Conectar com QR code
await gerenciador.iniciarConexao();

// Opção 2: Conectar com pairing code
await gerenciador.iniciarConexao('5511999999999');

Comparação: QR Code vs Pairing Code

| Aspecto | QR Code | Pairing Code | |----------|---------|--------------| | Formato | Imagem base64 + código | Código XXXX-XXXX | | Expiração | ~60 segundos | ~60 segundos | | Como usar | Escanear com câmera | Digitar no WhatsApp | | Dispositivo | Qualquer um com câmera | Apenas celular | | Regenerar | Chamar /instance/{id}/qr | Chamar /instance/{id}/pair | | Vantagem | Mais universal | Não precisa de câmera | | Desvantagem | Precisa de câmera | Apenas celular |


Dicas Importantes

  1. QR Codes expiram rapidamente (~60 segundos). Se o usuário não escanear a tempo, chame o endpoint novamente para gerar um novo.

  2. Pairing codes também expiram (~60 segundos). Gere o código apenas quando o usuário estiver pronto para digitar.

  3. Múltiplas chamadas ao endpoint /instance/{id}/qr podem gerar QR codes diferentes. Cada QR code invalida o anterior.

  4. Instância deve estar inicializada para gerar pairing code. Se não estiver, chame /instance/connect primeiro.

  5. Use reconexão antes de regenerar se a instância estiver com problemas persistentes.


4. Conectar Instância (POST /instance/{instanceId}/connect)

Descrição: Conecta uma instância existente, configurando webhook e eventos.

Endpoint:

POST /instance/{instanceId}/connect
Content-Type: application/json

Campos Opcionais

| Campo | Tipo | Descrição | |-------|------|-----------| | webhookUrl | string | URL do webhook para receber eventos | | subscribe | string[] | Lista de eventos para se inscrever | | immediate | boolean | Se deve conectar imediatamente | | phone | string | Número de telefone para pareamento | | rabbitmqEnable | string | Habilitar RabbitMQ (true ou false) | | websocketEnable | string | Habilitar WebSocket (true ou false) | | natsEnable | string | Habilitar NATS (true ou false) |


Exemplo de Uso

POST /instance/550e8400-e29b-41d4-a716-446655440000/connect
Content-Type: application/json

Corpo da requisição:

{
  "webhookUrl": "https://seu-sistema.com/webhook/whatsapp",
  "subscribe": ["MESSAGE", "RECEIPT", "BUTTON_CLICK"],
  "immediate": true,
  "phone": "5511999999999"
}

Resposta de Sucesso (HTTP 200)

{
  "message": "success",
  "data": {
    "jid": "[email protected]",
    "webhookUrl": "https://seu-sistema.com/webhook/whatsapp",
    "eventString": "MESSAGE,RECEIPT,BUTTON_CLICK"
  }
}

5. Obter Status da Instância (GET /instance/{instanceId}/status)

Descrição: Obtém o status atual da conexão da instância.

Endpoint:

GET /instance/{instanceId}/status

Resposta de Sucesso (HTTP 200)

{
  "message": "success",
  "data": {
    "instance": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Atendimento Comercial",
      "token": "api-token-12345",
      "jid": "[email protected]",
      "connected": true,
      "disconnect_reason": ""
    },
    "state": "open"
  }
}

| Estado | Descrição | |-------|-----------| | open | Instância conectada e pronta | | close | Instância desconectada |


Fluxo Completo de Criação e Conexão

1. Cliente envia requisição para /instance/create
   ↓
2. Evolution GO cria a instância no banco de dados
   ↓
3. Sistema inicia o cliente do WhatsApp automaticamente
   ↓
4. QR code é gerado automaticamente
   ↓
5. Cliente pode:
   - Buscar QR code via /instance/{id}/qr
   - Buscar código de pareamento via /instance/{id}/pair
   ↓
6. Usuário escaneia o QR code ou digita o código de pareamento
   ↓
7. WhatsApp estabelece a conexão
   ↓
8. Instância muda status para "connected" (open)
   ↓
9. Cliente pode começar a enviar mensagens

Reconexão de Instância

A Evolution GO fornece dois endpoints para reconectar instâncias desconectadas: reconexão simples (usando sessão existente) e reconexão forçada (com novo número de telefone).


1. Reconexão Simples (POST /instance/reconnect)

Descrição: Reconecta uma instância que foi desconectada, utilizando a sessão existente no dispositivo. O WhatsApp tenta restaurar a conexão com a mesma sessão sem precisar escanear QR code novamente.

Endpoint:

POST /instance/reconnect

Autenticação: Requer header apikey com o token da instância.


Comportamento Interno

Ao chamar o endpoint de reconexão, o sistema executa automaticamente:

  1. Desconecta o cliente existente (se houver) e remove o event handler
  2. Limpa os recursos da instância (client pointer, kill channel, cache de userInfo)
  3. Atualiza o status no banco de dados para connected: false, disconnect_reason: "Reconnecting"
  4. Aguarda 2 segundos para garantir limpeza completa
  5. Inicia uma nova instância como se fosse a primeira vez
  6. A sessão existente no dispositivo é restaurada automaticamente

Exemplo de Uso

POST /instance/reconnect
apikey: api-token-12345

Corpo da requisição: Não é necessário corpo.

Exemplo com cURL

curl -X POST "http://localhost:3000/instance/reconnect" \
  -H "apikey: api-token-12345"

Resposta de Sucesso (HTTP 200)

{
  "message": "success"
}

Mapeamento de Erros

| Erro | Status HTTP | Mensagem | Causa | Solução | |-------|-------------|----------|--------|----------| | Instância não encontrada | 500 | "instance not found" | Instance ID inválido ou token incorreto | Verificar o token da API no header | | Sessão não encontrada | 500 | "no active session found" | Não há sessão salva no servidor | 1. Tentar reconexão forçada<br>2. Escanear QR code novamente | | Cliente desconectado | 500 | "client disconnected" | Cliente existe mas está desconectado | Tentar novamente ou usar reconexão forçada | | Falha ao obter instância | 500 | "failed to get instance: ..." | Erro ao buscar instância no banco | Verificar logs do servidor |


2. Reconexão Forçada (POST /instance/forcereconnect/{instanceId})

Descrição: Força a reconexão de uma instância com um novo número de telefone. Atualiza o JID (identificador do WhatsApp) e reinicia completamente a conexão. Ideal para quando a sessão foi corrompida ou o número mudou.

Endpoint:

POST /instance/forcereconnect/{instanceId}
Content-Type: application/json

Autenticação: Requer API key de admin (configurada no servidor).


Campos Obrigatórios

| Campo | Tipo | Descrição | |-------|------|-----------| | number | string | Número de telefone para atualizar o JID (com DDI, ex: 5511999999999) |


Comportamento Interno

Ao chamar a reconexão forçada, o sistema executa:

  1. Verifica se já está conectado - Se já estiver conectado e logado, retorna erro
  2. Atualiza o JID no banco de dados para o novo número
  3. Desconecta o cliente existente completamente
  4. Remove todos os recursos da instância (client pointer, kill channel)
  5. Carrega a configuração de proxy (se existir)
  6. Inicia um novo cliente do WhatsApp do zero
  7. Aguarda 2 segundos para verificar se a conexão foi bem-sucedida
  8. Retorna sucesso ou erro conforme o resultado

Exemplo de Uso

POST /instance/forcereconnect/550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

Corpo da requisição:

{
  "number": "5511999999999"
}

Exemplo com cURL

curl -X POST "http://localhost:3000/instance/forcereconnect/550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "5511999999999"
  }'

Resposta de Sucesso (HTTP 200)

{
  "message": "success"
}

Mapeamento de Erros

| Erro | Status HTTP | Mensagem | Causa | Solução | |-------|-------------|----------|--------|----------| | Instance ID obrigatório | 400 | "instanceId is required" | Parâmetro instanceId vazio na URL | Informar o ID da instância | | Número obrigatório | 400 | "number is required" | Campo number não enviado | Informar o número de telefone | | Já conectado | 500 | "client already connected" | Instância já está conectada e logada | Não há ação necessária | | Falha ao atualizar JID | 500 | Erro do ForceUpdateJid | Número inválido ou instância não encontrada | Verificar o número e o instance ID | | Falha ao conectar | 500 | "failed to connect" | Cliente não conseguiu conectar após 2 segundos | 1. Verificar conexão com internet<br>2. Tentar novamente<br>3. Verificar se o proxy está acessível | | Erro interno | 500 | Erro genérico | Falha ao buscar instância ou erro no proxy | Verificar logs do servidor |


Comparação: Reconexão Simples vs Forçada

| Aspecto | Reconexão Simples | Reconexão Forçada | |----------|-------------------|-------------------| | Endpoint | POST /instance/reconnect | POST /instance/forcereconnect/{instanceId} | | Autenticação | Token da instância (header apikey) | API key de admin | | Corpo da requisição | Nenhum | {"number": "5511999999999"} | | Sessão existente | Mantém a sessão | Cria nova sessão | | Número de telefone | Mantém o mesmo | Atualiza para o novo | | Quando usar | Instância caiu mas sessão é válida | Sessão corrompida ou número mudou | | Precisa de QR code | Geralmente não | Pode ser necessário |


Quando Usar Cada Endpoint

Use Reconexão Simples quando:

  • A instância ficou offline temporariamente
  • O servidor reiniciou e precisa restaurar conexões
  • O WhatsApp desconectou por inatividade
  • A sessão ainda é válida no servidor

Use Reconexão Forçada quando:

  • A sessão foi corrompida ou expirou
  • O número de telefone mudou
  • A reconexão simples não funciona
  • Precisa associar a instância a um novo dispositivo
  • Há problemas persistentes de conexão

Boas Práticas

1. Monitoramento e Reconexão Automática

class MonitorInstancia {
  constructor(instanceId, apiToken) {
    this.instanceId = instanceId;
    this.apiToken = apiToken;
    this.baseUrl = 'http://localhost:3000';
    this.intervalId = null;
  }

  async verificarStatus() {
    try {
      const response = await fetch(
        `${this.baseUrl}/instance/status`,
        { headers: { 'apikey': this.apiToken } }
      );

      if (!response.ok) throw new Error('Erro ao verificar status');

      const result = await response.json();
      return result.data.state;

    } catch (error) {
      console.error('Erro ao verificar status:', error.message);
      return 'error';
    }
  }

  async reconectar() {
    try {
      console.log(`Tentando reconectar instância ${this.instanceId}...`);

      const response = await fetch(
        `${this.baseUrl}/instance/reconnect`,
        {
          method: 'POST',
          headers: { 'apikey': this.apiToken }
        }
      );

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error);
      }

      console.log('Reconexão iniciada com sucesso');
      return true;

    } catch (error) {
      console.error('Falha na reconexão:', error.message);
      return false;
    }
  }

  async reconectarForcado(numero) {
    try {
      console.log(`Tentando reconexão forçada com número ${numero}...`);

      const response = await fetch(
        `${this.baseUrl}/instance/forcereconnect/${this.instanceId}`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ number: numero })
        }
      );

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error);
      }

      console.log('Reconexão forçada iniciada com sucesso');
      return true;

    } catch (error) {
      console.error('Falha na reconexão forçada:', error.message);
      return false;
    }
  }

  iniciarMonitoramento(intervalMs = 60000) {
    console.log(`Iniciando monitoramento a cada ${intervalMs / 1000}s`);

    this.intervalId = setInterval(async () => {
      const status = await this.verificarStatus();

      if (status === 'close' || status === 'error') {
        console.warn(`Instância ${this.instanceId} desconectada. Tentando reconectar...`);
        await this.reconectar();

        // Aguarda 10 segundos e verifica novamente
        await new Promise(resolve => setTimeout(resolve, 10000));

        const novoStatus = await this.verificarStatus();
        if (novoStatus !== 'open') {
          console.error('Reconexão simples falhou. Considere usar reconexão forçada.');
        }
      }
    }, intervalMs);
  }

  pararMonitoramento() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
      console.log('Monitoramento parado');
    }
  }
}

// Uso
const monitor = new MonitorInstancia(
  '550e8400-e29b-41d4-a716-446655440000',
  'api-token-12345'
);

// Iniciar monitoramento a cada 60 segundos
monitor.iniciarMonitoramento(60000);

// Para parar: monitor.pararMonitoramento();

2. Reconexão com Fallback

async function reconectarComFallback(instanceId, apiToken, numeroFallback) {
  // Tentativa 1: Reconexão simples
  console.log('Tentando reconexão simples...');
  const reconnectResponse = await fetch(
    'http://localhost:3000/instance/reconnect',
    {
      method: 'POST',
      headers: { 'apikey': apiToken }
    }
  );

  if (reconnectResponse.ok) {
    // Aguarda 5 segundos para verificar se conectou
    await new Promise(resolve => setTimeout(resolve, 5000));

    const statusResponse = await fetch(
      'http://localhost:3000/instance/status',
      { headers: { 'apikey': apiToken } }
    );

    const statusResult = await statusResponse.json();
    if (statusResult.data.state === 'open') {
      console.log('Reconexão simples bem-sucedida!');
      return { success: true, method: 'simple' };
    }
  }

  // Tentativa 2: Reconexão forçada
  console.log('Reconexão simples falhou. Tentando reconexão forçada...');
  const forceResponse = await fetch(
    `http://localhost:3000/instance/forcereconnect/${instanceId}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ number: numeroFallback })
    }
  );

  if (forceResponse.ok) {
    // Aguarda 5 segundos para verificar
    await new Promise(resolve => setTimeout(resolve, 5000));

    const statusResponse = await fetch(
      'http://localhost:3000/instance/status',
      { headers: { 'apikey': apiToken } }
    );

    const statusResult = await statusResponse.json();
    if (statusResult.data.state === 'open') {
      console.log('Reconexão forçada bem-sucedida!');
      return { success: true, method: 'forced' };
    }
  }

  // Tentativa 3: Obter novo QR code
  console.log('Reconexão forçada falhou. Será necessário escanear QR code.');
  const qrResponse = await fetch(
    'http://localhost:3000/instance/qr',
    { headers: { 'apikey': apiToken } }
  );

  if (qrResponse.ok) {
    const qrResult = await qrResponse.json();
    return {
      success: false,
      method: 'qr_required',
      qrcode: qrResult.data.qrcode
    };
  }

  return { success: false, method: 'failed' };
}

// Uso
const resultado = await reconectarComFallback(
  '550e8400-e29b-41d4-a716-446655440000',
  'api-token-12345',
  '5511999999999'
);

if (!resultado.success && resultado.qrcode) {
  console.log('Escaneie o QR code para reconectar');
  // Exibir QR code na interface
}

3. Tratamento de Webhook de Desconexão

// No endpoint que recebe webhooks da Evolution GO
app.post('/webhook', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'LoggedOut') {
    console.warn(`Instância desconectada. Motivo: ${data.reason}`);

    // Agendar reconexão automática
    setTimeout(async () => {
      const resultado = await reconectarComFallback(
        req.body.instanceId,
        req.body.instanceToken,
        numeroPadrao
      );

      if (!resultado.success) {
        // Notificar administrador
        console.error('Não foi possível reconectar automaticamente');
        // enviarEmailAdmin(...);
      }
    }, 5000);
  }

  res.sendStatus(200);
});

Comparação com Pilot Status

Criação de Instância

Pilot Status:

POST /v1/instances
{
  "name": "Atendimento Vendas",
  "environment": "LIVE"
}

Limitações:

  • ❌ Não há configuração de proxy
  • ❌ Configurações avançadas limitadas
  • ❌ Pareamento apenas por QR code

Evolution GO:

POST /instance/create
{
  "instanceId": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Atendimento Vendas",
  "token": "api-token-12345",
  "proxy": {
    "protocol": "http",
    "host": "proxy.empresa.com",
    "port": "3128"
  },
  "advancedSettings": {
    "alwaysOnline": true,
    "rejectCall": true,
    "readMessages": true
  }
}

Vantagens:

  • Configuração de proxy completa (HTTP e SOCKS5)
  • Configurações avançadas extensíveis
  • Pareamento por código (sem QR code)
  • Controle total sobre o comportamento da instância

Boas Práticas

1. Tratamento de Erros na Criação

async function criarInstancia(dados) {
  try {
    const response = await fetch(
      'http://localhost:3000/instance/create',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(dados)
      }
    );

    if (!response.ok) {
      const error = await response.json();
      
      if (response.status === 400) {
        console.error('Erro de validação:', error.error);
        throw new Error(`Validação: ${error.error}`);
      } else if (response.status === 500) {
        if (error.error.includes('instance already exists')) {
          console.error('Instância já existe com esse nome');
          throw new Error('Instância já existe');
        } else {
          console.error('Erro interno:', error.error);
          throw new Error(`Erro: ${error.error}`);
        }
      }
    }

    const result = await response.json();
    console.log('Instância criada:', result.data.id);
    return result.data;

  } catch (error) {
    console.error('Erro ao criar instância:', error.message);
    throw error;
  }
}

// Uso
try {
  const instance = await criarInstancia({
    instanceId: '550e8400-e29b-41d4-a716-446655440000',
    name: 'Atendimento Vendas',
    token: 'api-token-12345',
    proxy: {
      protocol: 'http',
      host: 'proxy.empresa.com',
      port: '3128'
    }
  });
  
  // Aguarda um pouco para o QR code ser gerado
  await new Promise(resolve => setTimeout(resolve, 3000));
  
  // Busca o QR code
  const qr = await obterQrCode(instance.id);
  console.log('QR code obtido:', qr);
  
} catch (error) {
  console.error('Não foi possível criar instância:', error.message);
}

2. Exibir QR Code na Interface

async function obterExibirQrCode(instanceId) {
  try {
    const response = await fetch(
      `http://localhost:3000/instance/${instanceId}/qr`
    );
    
    if (!response.ok) {
      throw new Error('Erro ao buscar QR code');
    }

    const result = await response.json();
    const qrData = result.data.qrcode;

    // Criar container
    const container = document.createElement('div');
    container.style.margin = '20px';
    container.style.textAlign = 'center';

    // Criar imagem
    const img = document.createElement('img');
    img.src = qrData;
    img.style.width = '300px';
    img.style.border = '1px solid #ddd';
    img.style.borderRadius = '8px';

    // Criar botão para下载
    const downloadBtn = document.createElement('button');
    downloadBtn.textContent = '下载 QR Code';
    downloadBtn.style.marginTop = '10px';
    downloadBtn.style.padding = '10px 20px';
    downloadBtn.style.backgroundColor = '#25D366';
    downloadBtn.style.color = 'white';
    downloadBtn.style.border = 'none';
    downloadBtn.style.borderRadius = '4px';
    downloadBtn.style.cursor = 'pointer';

    downloadBtn.onclick = () => {
      const link = document.createElement('a');
      link.href = qrData;
      link.download = `qrcode-${instanceId}.png`;
      link.click();
    };

    // Adicionar elementos ao container
    container.appendChild(img);
    container.appendChild(downloadBtn);
    document.body.appendChild(container);

    return qrData;

  } catch (error) {
    console.error('Erro ao obter QR code:', error.message);
    throw error;
  }
}

// Uso
await obterExibirQrCode('550e8400-e29b-41d4-a716-446655440000');

3. Polling de Status da Instância

async function aguardarConexao(instanceId, maxTentativas = 30) {
  for (let tentativa = 1; tentativa <= maxTentativas; tentativa++) {
    try {
      const response = await fetch(
        `http://localhost:3000/instance/${instanceId}/status`
      );
      
      if (!response.ok) {
        throw new Error('Erro ao verificar status');
      }

      const result = await response.json();
      const status = result.data.state;

      console.log(`Tentativa ${tentativa}/${maxTentativas} - Status: ${status}`);

      if (status === 'open') {
        console.log('Instância conectada com sucesso!');
        return true;
      }

      // Aguarda 2 segundos antes da próxima verificação
      await new Promise(resolve => setTimeout(resolve, 2000));

    } catch (error) {
      console.error(`Erro na tentativa ${tentativa}:`, error.message);
      
      if (tentativa === maxTentativas) {
        throw new Error(`Falha após ${maxTentativas} tentativas: ${error.message}`);
      }
      
      await new Promise(resolve => setTimeout(resolve, 2000));
    }
  }

  throw new Error('Timeout: instância não conectou após 60 segundos');
}

// Uso
try {
  const conectada = await aguardarConexao('550e8400-e29b-41d4-a716-446655440000');
  if (conectada) {
    console.log('Pronto para enviar mensagens!');
  }
} catch (error) {
  console.error('Erro:', error.message);
}

4. Uso de Código de Pareamento

async function obterUsarCodigoPareamento(instanceId, numero) {
  try {
    const response = await fetch(
      `http://localhost:3000/instance/${instanceId}/pair`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ phone: numero })
      }
    );

    if (!response.ok) {
      throw new Error('Erro ao obter código de pareamento');
    }

    const result = await response.json();
    const codigo = result.data.pairingCode;

    console.log(`Código de pareamento obtido: ${codigo}`);

    // Exibir código para o usuário
    const container = document.createElement('div');
    container.style.padding = '20px';
    container.style.textAlign = 'center';
    container.style.fontFamily = 'Arial, sans-serif';

    container.innerHTML = `
      <h2>📱 Pareamento por Telefone</h2>
      <p style="font-size: 24px; letter-spacing: 4px; margin: 20px 0;">
        <strong>${codigo}</strong>
      </p>
      <p>Use o WhatsApp no seu celular:</p>
      <ol style="text-align: left; margin: 20px auto; max-width: 500px;">
        <li>Abra o WhatsApp</li>
        <li>Vá em <strong>Menu → Aparelhos Conectados</strong></li>
        <li>Toque em <strong>Conectar um aparelho</strong></li>
        <li>Toque em <strong>Conectar usando número de telefone</strong></li>
        <li>Digite o código: <strong>${codigo}</strong></li>
      </ol>
    `;

    document.body.appendChild(container);

    return codigo;

  } catch (error) {
    console.error('Erro ao obter código de pareamento:', error.message);
    throw error;
  }
}

// Uso
await obterUsarCodigoPareamento('550e8400-e29b-41d4-a716-446655440000', '5511999999999');

5. Configuração de Proxy com Fallback

async function criarInstanciaComProxyFallback(dados) {
  const dadosSemProxy = { ...dados };
  delete dadosSemProxy.proxy;

  try {
    // Tenta criar com proxy
    return await criarInstancia(dados);
  } catch (error) {
    console.warn('Falha ao criar com proxy, tentando sem:', error.message);
    
    // Fallback: tenta sem proxy
    return await criarInstancia(dadosSemProxy);
  }
}

// Uso
await criarInstanciaComProxyFallback({
  instanceId: '550e8400-e29b-41d4-a716-446655440000',
  name: 'Atendimento',
  token: 'api-token-12345',
  proxy: {
    protocol: 'http',
    host: 'proxy.empresa.com',
    port: '3128',
    username: 'user',
    password: 'pass'
  }
});

6. Gerenciamento Múltiplas Instâncias

class GerenciadorInstancias {
  constructor() {
    this.instances = new Map();
  }

  async criar(dados) {
    const instance = await criarInstancia(dados);
    this.instances.set(instance.id, instance);
    return instance;
  }

  async obterStatus(instanceId) {
    const response = await fetch(
      `http://localhost:3000/instance/${instanceId}/status`
    );
    const result = await response.json();
    return result.data.state;
  }

  async obterQrCode(instanceId) {
    const response = await fetch(
      `http://localhost:3000/instance/${instanceId}/qr`
    );
    const result = await response.json();
    return result.data;
  }

  async listarTodas() {
    const response = await fetch('http://localhost:3000/instance/all');
    const result = await response.json();
    return result.data;
  }

  async instanciasConectadas() {
    const instances = await this.listarTodas();
    return instances.filter(inst => inst.connected);
  }

  async instanciasDesconectadas() {
    const instances = await this.listarTodas();
    return instances.filter(inst => !inst.connected);
  }
}

// Uso
const gerenciador = new GerenciadorInstancias();

// Criar múltiplas instâncias
await gerenciador.criar({
  instanceId: 'inst-1',
  name: 'Vendas',
  token: 'token-1'
});

await gerenciador.criar({
  instanceId: 'inst-2',
  name: 'Suporte',
  token: 'token-2'
});

// Listar instâncias conectadas
const conectadas = await gerenciador.instanciasConectadas();
console.log('Instâncias conectadas:', conectadas);