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.messageId→Evolution GO.data.IDPilot Status.internalMessageId→idusado 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.replyContent→Evolution GO.data.Message.extendedTextMessage.textPilot Status.quotedMessageId→Evolution GO.data.quoted.stanzaIDPilot Status.messageRepliedId→ Correlação deve ser feita buscando o envio original viastanzaID
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.messageId→Evolution GO.data.messageIds[0]Pilot Status.readAt→Evolution 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
quotedMessageIdemessageRepliedId - Evolution GO: Apenas
quoted.stanzaID(ID do WhatsApp). A correlação interna deve ser implementada pelo consumidor do webhook
3. Identificador Interno
- Pilot Status:
internalMessageIdretorna sempre em webhooks de mensagem - Evolution GO: O
idfornecido no envio via API não retorna automaticamente no webhook. UseID(do WhatsApp) como chave principal
4. Ambiente
- Pilot Status: Campo
data.environmentindica 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
ButtonClickcom 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
MESSAGEcomo padrão
Considerações Finais
-
Correlação: A Evolution GO não fornece correlação interna automática. Implemente seu próprio mapeamento usando o
IDdo WhatsApp como chave. -
Eventos: A Pilot Status tem eventos mais granulares, enquanto Evolution GO agrupa múltiplos tipos em eventos genéricos.
-
Botões: A Evolution GO trata cliques em botões como evento separado (
ButtonClick), algo que não existe na Pilot Status. -
Mídia: Ambas as plataformas suportam mídia, mas a estrutura do payload difere.
-
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:
- multipart/form-data - Upload direto do arquivo
- 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:
-
Detecta o tipo de conversão:
- API externa (MinIO): Se
config.ApiAudioConverterestiver configurado - Conversão local (FFmpeg): Se
ffmpegestiver disponível no sistema
- API externa (MinIO): Se
-
Conversão via API (MinIO):
- Faz POST multipart para a URL configurada
- Envia o áudio como base64 no campo
base64OU como URL no campourl - Recebe o áudio convertido em formato
.ogg(Opus codec) - Recebe a duração do áudio em segundos
-
Conversão via FFmpeg (local):
- Executa FFmpeg para converter para
.oggcom codec Opus - Comandos:
ffmpeg -i input.mp3 -f ogg -vn -acodec libopus -b:a 128k output.ogg - Detecta automaticamente a duração do áudio
- Executa FFmpeg para converter para
-
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
-
Exibe "gravando áudio...":
- Antes do envio, envia estado
composingcom mídiaaudio - Aguarda o tempo configurado no campo
delay - Envia a mensagem
- Envia estado
pausedpara parar o indicador
- Antes do envio, envia estado
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:
- Exibe "gravando áudio..." por 3 segundos
- Envia o áudio
- 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:
- Converte o áudio para formato
.ogg - Envia sem criptografia (upload
UploadNewsletter) - Não adiciona PTT (Push-to-Talk) na mensagem
- 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
.oggantes
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
delayouChatPresence - 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:
- O sistema envia o estado
"composing"para o destinatário - Aguarda o tempo especificado em milissegundos
- Envia a mensagem
- 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:
- Exibe "digitando..." para o destinatário
- Aguarda 3 segundos (3000ms)
- Envia a mensagem
- 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:
- Exibe "gravando áudio..." para o destinatário
- Aguarda 5 segundos (5000ms)
- Envia o áudio
- 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
delaynos 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
delayem 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
- Tentativa 1: Envio inicial
- Se falhar com erro de conexão:
- Tenta reconectar a instance
- Aguarda 1-2 segundos
- Tentativa 2
- Se falhar novamente:
- Tenta reconectar novamente
- Aguarda 2-4 segundos (backoff progressivo)
- Tentativa 3
- 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:
- Status HTTP: Pilot Status usa 202 (aceito), Evolution GO usa 200 (sucesso)
- ID do envio: Pilot Status retorna
idinterno, Evolution GO retornaIDdo WhatsApp - Formato do número: Pilot Status usa
+55..., Evolution GO usa55... - 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:
- Detecção de URL: Procura URLs no campo
textou usa o campourl - Download da página: Faz HTTP GET para a URL
- Parse HTML: Extrai metadados das tags Open Graph:
<title>ouog:title<meta name="description">ouog:description<meta property="og:image">
- Download da imagem: Se encontrou
og:image, faz download da imagem - Construção do preview: Monta o ExtendedTextMessage com os metadados
- 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:
- Verifica se há cliente ativo para a instância
- 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
- Se o cliente existe mas não está conectado:
- Verifica se já existe QR code gerado
- Busca o QR code no banco de dados
- Se não há QR code ainda:
- Aguarda 2 segundos adicionais
- Tenta buscar novamente
- 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
- O sistema chama a API do WhatsApp para gerar um código vinculado ao número
- O código tem formato
XXXX-XXXX(8 caracteres alfanuméricos) - O código é exibido para o usuário
- No celular, o usuário vai em Aparelhos Conectados → Conectar usando número de telefone
- O usuário digita o código no WhatsApp
- 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:
- Abra o WhatsApp
- Vá em Menu → Aparelhos Conectados
- Toque em Conectar um aparelho
- Toque em Conectar usando número de telefone
- Digite o código recebido:
J7K3-N5P2 - 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
-
QR Codes expiram rapidamente (~60 segundos). Se o usuário não escanear a tempo, chame o endpoint novamente para gerar um novo.
-
Pairing codes também expiram (~60 segundos). Gere o código apenas quando o usuário estiver pronto para digitar.
-
Múltiplas chamadas ao endpoint
/instance/{id}/qrpodem gerar QR codes diferentes. Cada QR code invalida o anterior. -
Instância deve estar inicializada para gerar pairing code. Se não estiver, chame
/instance/connectprimeiro. -
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:
- Desconecta o cliente existente (se houver) e remove o event handler
- Limpa os recursos da instância (client pointer, kill channel, cache de userInfo)
- Atualiza o status no banco de dados para
connected: false,disconnect_reason: "Reconnecting" - Aguarda 2 segundos para garantir limpeza completa
- Inicia uma nova instância como se fosse a primeira vez
- 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:
- Verifica se já está conectado - Se já estiver conectado e logado, retorna erro
- Atualiza o JID no banco de dados para o novo número
- Desconecta o cliente existente completamente
- Remove todos os recursos da instância (client pointer, kill channel)
- Carrega a configuração de proxy (se existir)
- Inicia um novo cliente do WhatsApp do zero
- Aguarda 2 segundos para verificar se a conexão foi bem-sucedida
- 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);