Evolution API: enviando todos os tipos de mensagem
Olá meus Unicórnios! 🦄✨
Mandar um "Olá, mundo" pelo WhatsApp é fácil: uma rota, dois campos, pronto. Só que quase nenhum sistema de verdade manda só texto. 😅 Ele manda o comprovante em imagem, o áudio do atendente, o vídeo do produto, o endereço da loja, o cartão do suporte, e aquela enquete para o time decidir onde vai o almoço.
Então fui atrás dos nove tipos de mensagem da Evolution API, um por um, com o objetivo de ter no fim um arquivo só, simples, que manda qualquer um deles. E encontrei pelo caminho uma armadilha que me deixou de olho arregalado: a API responde que deu tudo certo em um caso em que o WhatsApp recusa a mensagem. 🤯
Se você só for ler um pedaço deste artigo, leia a seção do status: ERROR. 🙏
🗺️ O mapa: uma rota por tipo
A primeira coisa que ajuda a organizar a cabeça é que não existe uma rota genérica de "enviar mensagem" com um campo de tipo. A Evolution tem uma rota por tipo, e todas seguem o mesmo desenho:
POST http://SEU_IP_AQUI:8080/message/<rota>/<nome-da-instancia>
O que muda de um tipo para o outro é o nome da rota e o corpo em JSON. O cabeçalho é sempre o mesmo: a sua chave em apikey e o Content-Type dizendo que vai JSON.
Este é o mapa completo, e vale deixar aberto numa aba enquanto você lê o resto:
Tipo de mensagem Rota Campo principal
────────────────── ─────────────────── ───────────────────
Texto sendText text
Imagem sendMedia media + mediatype
Áudio de voz sendWhatsAppAudio audio
Vídeo sendMedia media + mediatype
Localização sendLocation latitude/longitude
Contato sendContact contact (lista)
Botões sendButtons buttons (lista)
Lista sendList sections (lista)
Enquete sendPoll values (lista)
Repare que imagem e vídeo dividem a mesma rota, o sendMedia, e que o áudio tem uma rota só dele. Isso não é capricho, e é a primeira coisa que confunde: já explico o porquê na seção do áudio.
💬 Texto: o mais simples de todos
Vamos começar pelo que tem dois campos, para você conferir que a sua instância está mesmo conversando antes de partir para os tipos complicados. O number é para quem vai, e o text é o que vai:
curl -X POST "http://SEU_IP_AQUI:8080/message/sendText/tutorial" \
-H "apikey: SUA_CHAVE_AQUI" \
-H 'Content-Type: application/json' \
-d '{"number":"SEU_NUMERO_AQUI","text":"Ola! Mensagem de texto simples."}'
Se você nunca colou um comando desses no terminal: o \ no fim da linha significa "o comando continua na linha de baixo", então copie o bloco inteiro de uma vez. E no terminal do Linux o atalho de colar é Ctrl+Shift+V ou o botão direito do mouse, porque o Ctrl+V comum não funciona ali e trava todo mundo na primeira tentativa. 😉
A resposta, formatada para ficar legível, vem assim:
{
"key": {
"remoteJid": "SEU_NUMERO_AQUI",
"fromMe": true,
"id": "3EB01470519A97AD59A70A"
},
"pushName": "Você",
"status": "PENDING",
"message": {
"conversation": "Ola! Mensagem de texto simples."
},
"messageType": "conversation",
"messageTimestamp": 1787070218,
"source": "web"
}
Guarde dois campos desse JSON, porque eles voltam no fim do artigo e são a chave da armadilha:
id: o identificador da mensagem dentro do WhatsApp. É por ele que você descobre depois se ela chegou.status: PENDING: repare que não diz "enviada". Diz pendente. A API está avisando, com todas as letras, que a história ainda não acabou.
Sobre o number: para uma pessoa, o formato é o número com código do país e sem nada mais (5547999990000), que a Evolution completa para [email protected]. Para um grupo, é o identificador dele com o sufixo @g.us. É um detalhe bobo com uma consequência séria, então volto a ele no fim.
🖼️ Imagem: base64 ou URL, e quem baixa o arquivo
A imagem vai pelo sendMedia, com o mediatype dizendo que é imagem. O campo media aceita duas coisas bem diferentes: o arquivo em base64 ou uma URL.
Começando pelo base64, que é o caminho que não depende de nada externo:
{
"number": "SEU_NUMERO_AQUI",
"mediatype": "image",
"mimetype": "image/jpeg",
"caption": "Uma imagem com legenda",
"fileName": "imagem.jpg",
"media": "/9j/4AAQSkZJRgABAQAAAQABAAD..."
}
⚠️ Aqui mora a primeira pegadinha: o base64 vai puro. Se você está acostumado com HTML, a tentação é colar aquele valor que começa com data:image/jpeg;base64,, do jeito que se usa num src de <img>. Com o prefixo, a chamada volta com 400. Corte tudo até a vírgula, inclusive.
A legenda vai no caption e é opcional. O fileName é o nome que aparece se a pessoa salvar o arquivo.
A URL e a surpresa do 403
Passar uma URL no media parece mais prático, e é, mas tem um detalhe que muda tudo: quem baixa a imagem é o servidor da Evolution, não o WhatsApp e não a máquina que fez a chamada. É o seu servidor que sai na internet buscar aquele endereço.
Descobri isso do jeito mais direto possível: apontei para uma imagem de um site grande e recebi isto de volta.
{
"status": 500,
"error": "Internal Server Error",
"response": {
"message": [
"AxiosError: Request failed with status code 403"
]
}
}
Repare no detalhe cruel: são dois códigos de erro na mesma resposta. O 500 é o que a Evolution te devolve, e o 403, escondido lá dentro, é o que o site respondeu para ela. Muitos sites recusam download feito por servidor, sem navegador nem referência, e é exatamente o que aconteceu.
A consequência prática é maior do que parece: quando a URL falha, a mensagem inteira se perde, não só a imagem. Se o arquivo é seu e está na máquina, o base64 evita esse risco de uma vez, porque não depende de o servidor conseguir alcançar site nenhum.
E se você omitir o mimetype, funciona: a Evolution deduz pelo mediatype e pela extensão do fileName. Mas preencher é mais seguro, e não custa nada.
🎤 Áudio: por que existe uma rota só para ele
Esta é a parte que mais gera dúvida, e a resposta é bem concreta. O áudio tem duas formas de chegar no WhatsApp:
- Pelo
sendWhatsAppAudio: chega como mensagem de voz, aquele balão com a ondinha e o botão de tocar, igual a um áudio gravado na hora. - Pelo
sendMediacommediatype: "audio": chega como arquivo anexado, que o WhatsApp mostra como documento com um clipe do lado.
Ou seja: a rota não muda o arquivo, muda o que a mensagem é. Para o áudio parecer gravado, é o sendWhatsAppAudio. E ele é simpático de usar, porque tem só dois campos:
{
"number": "SEU_NUMERO_AQUI",
"audio": "T2dnUwACAAAAAAAAAAA..."
}
O formato que o WhatsApp usa para voz é OGG com codec Opus. Se você tem um arquivo em outro formato, o ffmpeg converte numa linha:
ffmpeg -i audio-original.mp3 -c:a libopus -b:a 32k audio.ogg
Uma coisa boa que descobri testando: a Evolution converte sozinha. Mandei um arquivo .m4a (que é AAC, não Opus) e a resposta veio com o áudio já reempacotado:
"mimetype": "audio/ogg; codecs=opus"
Ela pegou o AAC e devolveu OGG/Opus, sem eu pedir nada. Isso salva bastante trabalho, mas continua valendo converter você mesmo quando o áudio é longo ou o volume é grande: a conversão consome CPU do seu servidor, e é melhor que ela aconteça uma vez, na sua máquina, do que a cada envio.
🎬 Vídeo: mesma rota da imagem, um campo diferente
Depois de entender o sendMedia, o vídeo é de graça: é o mesmo corpo, trocando duas palavras.
{
"number": "SEU_NUMERO_AQUI",
"mediatype": "video",
"mimetype": "video/mp4",
"caption": "Um video com legenda",
"fileName": "video.mp4",
"media": "AAAAIGZ0eXBpc29tAAACAGlzb21pc28y..."
}
Vale um aviso de bom senso sobre tamanho. O base64 engorda o arquivo em cerca de 33%, porque ele representa cada 3 bytes com 4 caracteres. Um vídeo de 15 MB viaja como uns 20 MB de texto dentro do JSON, e isso passa pela memória do seu servidor. Para vídeo grande, a URL é mais sensata que o base64, desde que ela esteja num lugar que responda ao seu servidor sem 403.
📍 Localização: quatro campos e nenhuma surpresa
A localização é o tipo mais tranquilo do artigo. Dois campos de texto para a pessoa ler, e as duas coordenadas:
{
"number": "SEU_NUMERO_AQUI",
"name": "Praca Central",
"address": "Rua das Flores, 100",
"latitude": -26.3044,
"longitude": -48.8487
}
As coordenadas vão como número, não como texto: -26.3044, sem aspas, com ponto decimal e não vírgula. E são obrigatórias, com um erro bem claro quando faltam:
{
"status": 400,
"error": "Bad Request",
"response": {
"message": [
[
"instance requires property \"latitude\""
]
]
}
}
Esse formato de erro vale a pena reconhecer, porque ele volta em todos os tipos: a Evolution valida o corpo contra um esquema antes de tentar mandar qualquer coisa, e a mensagem diz literalmente qual propriedade está faltando. É o tipo de erro que se resolve lendo. 🙂
O name e o address são só rótulos: quem manda no alfinete do mapa são a latitude e a longitude. Se você trocar as duas de lugar, a mensagem chega perfeita, com o nome certinho, apontando para o meio do oceano. Não há erro nenhum para te avisar.
👤 Contato: o vCard que a Evolution monta para você
O contato tem uma particularidade: o campo contact é uma lista, porque você pode mandar vários de uma vez, cada um sendo um objeto.
{
"number": "SEU_NUMERO_AQUI",
"contact": [
{
"fullName": "Suporte da Loja",
"wuid": "5547999990000",
"phoneNumber": "+55 47 99999-0000",
"organization": "Loja Exemplo",
"email": "[email protected]"
}
]
}
A dúvida certa aqui é: por que dois campos de telefone? Isso mesmo, dois! Eles têm papéis diferentes:
wuid: o número limpo, só dígitos. É o que o WhatsApp usa internamente para reconhecer que aquele contato tem conta, e é o que faz aparecer o botão de conversar.phoneNumber: o número como a pessoa vai ler na tela. Aqui pode ter espaço, parêntese e traço.
A parte boa é que você não escreve vCard nenhum: a Evolution monta e mostra na resposta o que foi gerado.
BEGIN:VCARD
VERSION:3.0
N:Suporte da Loja
FN:Suporte da Loja
ORG:Loja Exemplo;
EMAIL:[email protected]
item1.TEL;waid=5547999990000:+55 47 99999-0000
item1.X-ABLabel:Celular
END:VCARD
Repare na linha do telefone, que é onde os dois campos se encontram: o waid= recebeu o wuid, e depois dos dois-pontos ficou o texto formatado. É exatamente por isso que os dois existem.
🔘 Botões: três, e nem um a mais
Agora entramos nos tipos interativos, que são os mais úteis e os mais cheios de regra. Os botões vão assim:
{
"number": "SEU_NUMERO_AQUI",
"title": "Pedido #1234",
"description": "Confirma o seu pedido?",
"footer": "Loja Exemplo",
"buttons": [
{ "type": "reply", "displayText": "Confirmar", "id": "sim" },
{ "type": "reply", "displayText": "Cancelar", "id": "nao" }
]
}
O displayText é o que a pessoa lê no botão, e o id é o código que volta para você quando ela toca. Esse id é a peça que faz o botão valer a pena: é ele que o seu sistema recebe e usa para saber o que fazer, sem tentar adivinhar o que a pessoa digitou.
E aqui vem o primeiro limite duro, com a mensagem de erro mais direta que encontrei em toda a API. Tentei quatro botões:
{
"status": 400,
"error": "Bad Request",
"response": {
"message": [
"Maximum of 3 reply buttons allowed"
]
}
}
Três botões de resposta, é o teto. Não é limitação da Evolution sendo conservadora: é o que o WhatsApp aceita. Se você tem mais de três opções para oferecer, o caminho é a lista, que vem na próxima seção.
Os outros três tipos de botão
O type: "reply" é o que devolve resposta, mas não é o único. Há três outros, e cada um usa um campo próprio em vez do id:
{
"number": "SEU_NUMERO_AQUI",
"title": "Loja Exemplo",
"description": "Fale com a gente",
"buttons": [
{ "type": "url", "displayText": "Abrir o site", "url": "https://exemplo.com" },
{ "type": "call", "displayText": "Ligar", "phoneNumber": "5547999990000" },
{ "type": "copy", "displayText": "Copiar cupom", "copyCode": "UNICORNIO10" }
]
}
O url abre um link, o call abre o discador com o número pronto, e o copy copia um código para a área de transferência, que é ótimo para cupom e para código de confirmação. Nenhum dos três devolve nada para o seu sistema, porque a ação acontece no celular da pessoa.
Uma curiosidade sobre o que a Evolution monta
Se você olhar a resposta de um envio de botões, vai encontrar uma estrutura bem diferente do que mandou:
{
"message": {
"viewOnceMessage": {
"message": {
"interactiveMessage": {
"body": {
"text": "*Pedido #1234*\n\nConfirma o seu pedido?\n"
},
"footer": { "text": "Loja Exemplo" },
"nativeFlowMessage": {
"buttons": [
{
"name": "quick_reply",
"buttonParamsJson": "{\"display_text\":\"Confirmar\",\"id\":\"sim\"}"
}
]
}
}
}
}
},
"messageType": "viewOnceMessage"
}
Duas coisas aparecem aí. A primeira: o title que você mandou virou negrito no corpo do texto (*Pedido #1234*), seguido de duas quebras de linha e da descrição. Não existe campo de título separado na mensagem final; a Evolution simplesmente monta um texto só. Saber isso evita você quebrar a cabeça achando que o título não apareceu do jeito certo, quando é assim que ele funciona.
A segunda: o tipo é viewOnceMessage, aquele mesmo do "ver uma vez" das fotos. É um empacotamento que o WhatsApp usa para as mensagens interativas, e não significa que a mensagem vai desaparecer.
O esquema antigo que você vai achar em tutorial velho
Se você pesquisar botões da Evolution, vai encontrar exemplos com buttonText e buttonId. Esse era o formato da versão 1, e ele foi trocado. Hoje ele responde:
{
"status": 400,
"error": "Bad Request",
"response": {
"message": [
[
"buttons[0] requires property \"type\""
]
]
}
}
Esse erro é uma boa notícia disfarçada: ele diz exatamente o que fazer. Se você copiou um exemplo e recebeu isso, é tutorial antigo. Acrescente o type e troque buttonText por displayText e buttonId por id.
📋 Listas: o campo obrigatório que ninguém adivinha
A lista é o caminho para quando três botões não bastam. Ela chega como uma mensagem com um botão que, ao ser tocado, abre um menu com seções e itens.
{
"number": "SEU_NUMERO_AQUI",
"title": "Nosso menu",
"description": "Toque para ver os itens",
"footerText": "Loja Exemplo",
"buttonText": "Ver opcoes",
"sections": [
{
"title": "Doces",
"rows": [
{ "title": "Bolo de cenoura", "description": "Fatia", "rowId": "bolo" },
{ "title": "Brigadeiro", "description": "Unidade", "rowId": "briga" }
]
}
]
}
A estrutura tem três níveis, e é bom não confundir: a mensagem tem seções, cada seção tem um título e uma lista de itens, e cada item (row) tem título, descrição e o rowId, que é o equivalente ao id do botão: o código que volta para você.
E tem uma pegadinha de campo obrigatório que me custou algumas tentativas. Repare que nos botões o rodapé se chama footer e é opcional. Na lista ele se chama footerText e é obrigatório:
{
"status": 400,
"error": "Bad Request",
"response": {
"message": [
[
"instance requires property \"footerText\""
]
]
}
}
Nome diferente e obrigatoriedade diferente, para a mesma coisa, em dois tipos vizinhos. É o tipo de detalhe que só se descobre batendo o nariz, e é o motivo de a validação da Evolution ser uma amiga: ela diz o nome exato da propriedade que falta. O rowId de cada item também é obrigatório, com o mesmo formato de erro.
⚠️ O status ERROR: quando o 200 mente
Chegamos na parte que me fez repensar como eu confiro envio. 😳
Todos os nove tipos foram aceitos pela API. Todos devolveram um JSON bonito, com id, com status: PENDING, sem nenhum campo de erro. Se eu tivesse parado ali, teria publicado que os nove funcionam.
Só que o PENDING estava falando sério. Fui olhar o log da Evolution e encontrei isto:
event: 'messages.update',
instance: 'tutorial',
data: {
keyId: '3EB0BC5BF1AF31E63F2B4C',
remoteJid: 'SEU_NUMERO_AQUI',
fromMe: true,
status: 'ERROR',
}
Aquele keyId é o id que a chamada da lista tinha me devolvido, com HTTP 200, minutos antes. A mensagem foi aceita pela Evolution, repassada ao WhatsApp, e o WhatsApp a recusou. Sem que nada na resposta original indicasse isso.
Repeti o envio dos nove tipos algumas vezes para ter certeza de que não era acaso, comparando cada id devolvido com os eventos que chegaram depois. O resultado foi idêntico em todas as rodadas: um único ERROR, sempre o da lista. Os outros oito tipos seguiram para DELIVERY_ACK, que é o WhatsApp confirmando a entrega.
Então é isso que está acontecendo com as listas hoje: o WhatsApp não está mais aceitando listMessage vindo dessa forma. A rota existe, a validação passa, o JSON de resposta é perfeito, e a mensagem não chega. Se você precisa de um menu com mais de três opções, o caminho que funciona é oferecer as opções em texto numerado e ler o número que a pessoa responder, ou dividir em botões de três em três.
Por que o 200 não é confirmação de entrega
Esta é a lição que vale para todos os tipos, e ela é estrutural, não um bug. Olhe o caminho que uma mensagem faz:
seu codigo ──POST──> Evolution ──WhatsApp Web──> WhatsApp
(responde 200 aqui) (aceita ou recusa aqui)
O 200 nasce no meio do caminho. Ele confirma que a Evolution entendeu o seu pedido, validou o corpo e repassou adiante. O que o WhatsApp vai fazer com a mensagem acontece depois, de forma assíncrona, e chega por outro canal: o evento messages.update.
Os estados que você vai ver nesse evento são estes:
PENDING saiu daqui, aguardando o WhatsApp
SERVER_ACK o servidor do WhatsApp recebeu
DELIVERY_ACK entregou no aparelho de destino
READ a pessoa leu
ERROR o WhatsApp recusou a mensagem
Ou seja: quem confirma entrega é o DELIVERY_ACK, nunca o código HTTP. Para acompanhar isso num sistema de verdade, o caminho é configurar um webhook e escutar o messages.update, que é assunto para um artigo inteiro. Mas para testar durante o desenvolvimento, o log do contêiner resolve:
docker logs evolution-api --since 5m | grep -A 8 "messages.update"
O --since 5m mostra só os últimos cinco minutos, e o -A 8 mostra as oito linhas seguintes a cada ocorrência, que é onde o keyId e o status aparecem. Guarde o id que a API devolveu e procure por ele: é assim que você separa "a mensagem não chegou" de "a mensagem nunca foi aceita".
📊 Enquetes: de 2 a 10, e um zero que muda o tipo
Fecho com o tipo mais divertido. A enquete tem três campos:
{
"number": "SEU_NUMERO_AQUI",
"name": "Qual sabor voce prefere?",
"selectableCount": 1,
"values": ["Chocolate", "Morango", "Baunilha"]
}
O name é a pergunta, o values são as alternativas, e o selectableCount diz quantas cada pessoa pode marcar: 1 é voto único, 2 permite marcar duas, e assim por diante.
Os limites são cobrados na validação, com mensagens que dizem o número exato. Com uma alternativa só:
{
"status": 400,
"error": "Bad Request",
"response": {
"message": [
[
"values does not meet minimum length of 2"
]
]
}
}
E com treze:
{
"status": 400,
"error": "Bad Request",
"response": {
"message": [
[
"values does not meet maximum length of 10"
]
]
}
}
De 2 a 10 alternativas, portanto. Faz sentido: uma alternativa não é uma pergunta, e onze não cabem na tela do celular.
O zero que troca o tipo da mensagem sem avisar
Este achado eu gosto especialmente, porque não dá erro nenhum. Mandei selectableCount: 0 só para ver o que acontecia, esperando um 400. Não veio erro: veio uma mensagem de outro tipo.
Com selectableCount: 1, a resposta traz:
"messageType": "pollCreationMessageV3"
Com selectableCount: 0, a mesma rota devolve:
"messageType": "pollCreationMessage"
Repare no V3 que desapareceu. É a versão antiga do formato de enquete do WhatsApp, e ela sai calada, sem aviso nenhum. Um zero acidental num campo numérico, talvez uma variável que veio vazia do seu banco, e a sua enquete vira uma mensagem de formato antigo. Sempre mande 1 quando quiser voto único.
🧩 Um arquivo só, para os nove tipos
Depois de mapear tudo, juntei num arquivo único e linear. É Node puro, com fetch nativo, então não precisa instalar nada: nenhum npm install, nenhuma dependência. Salve como enviar.js e pronto.
Primeiro, a configuração e a função por onde todo envio passa:
const fs = require("fs");
const API = process.env.EVO_URL || "http://SEU_IP_AQUI:8080";
const CHAVE = process.env.EVO_KEY || "SUA_CHAVE_AQUI";
const INSTANCIA = process.env.EVO_INSTANCIA || "tutorial";
const DESTINO = process.env.EVO_DESTINO || "SEU_NUMERO_AQUI";
// Todo envio passa por aqui. A Evolution responde 200 mesmo quando o
// WhatsApp vai recusar depois, por isso imprimimos o id: e por ele que
// voce encontra a mensagem no log se ela nao chegar.
async function enviar(rota, corpo) {
const resposta = await fetch(API + "/message/" + rota + "/" + INSTANCIA, {
method: "POST",
headers: {
"apikey": CHAVE,
"Content-Type": "application/json"
},
body: JSON.stringify(corpo)
});
const dados = await resposta.json();
if (resposta.status !== 201 && resposta.status !== 200) {
console.log("ERRO " + resposta.status + " em " + rota + ":");
console.log(JSON.stringify(dados, null, 4));
return null;
}
console.log("ok " + rota + " id=" + dados.key.id);
return dados;
}
// O arquivo vai em base64 puro, SEM o prefixo "data:image/jpeg;base64,".
// Com o prefixo a Evolution responde 400.
function paraBase64(caminho) {
return fs.readFileSync(caminho).toString("base64");
}
Repare que a chave e o IP saem de variável de ambiente, com um valor de reserva. Isso não é firula: é o que evita você commitar a sua apikey por distração, e ela dá acesso a mandar mensagem em nome do seu WhatsApp.
Agora as nove funções. Cada uma monta o corpo do seu tipo e entrega para a enviar(), com um comentário curto onde mora a armadilha:
function enviarTexto(texto) {
return enviar("sendText", { number: DESTINO, text: texto });
}
function enviarImagem(caminho, legenda) {
return enviar("sendMedia", {
number: DESTINO,
mediatype: "image",
mimetype: "image/jpeg",
caption: legenda,
fileName: "imagem.jpg",
media: paraBase64(caminho)
});
}
// Audio de voz tem rota propria. O sendMedia com mediatype "audio"
// manda como arquivo anexado, nao como aquele balaozinho de gravacao.
function enviarAudio(caminho) {
return enviar("sendWhatsAppAudio", {
number: DESTINO,
audio: paraBase64(caminho)
});
}
function enviarVideo(caminho, legenda) {
return enviar("sendMedia", {
number: DESTINO,
mediatype: "video",
mimetype: "video/mp4",
caption: legenda,
fileName: "video.mp4",
media: paraBase64(caminho)
});
}
function enviarLocalizacao(nome, endereco, latitude, longitude) {
return enviar("sendLocation", {
number: DESTINO,
name: nome,
address: endereco,
latitude: latitude,
longitude: longitude
});
}
function enviarContato(nome, numero, empresa) {
return enviar("sendContact", {
number: DESTINO,
contact: [{
fullName: nome,
// wuid e o numero limpo, so digitos. E o que faz o WhatsApp
// reconhecer o contato e mostrar o botao de conversar.
wuid: numero,
phoneNumber: numero,
organization: empresa
}]
});
}
// No maximo 3 botoes. O quarto derruba a chamada com 400.
function enviarBotoes(titulo, texto, rodape, botoes) {
return enviar("sendButtons", {
number: DESTINO,
title: titulo,
description: texto,
footer: rodape,
buttons: botoes
});
}
// footerText e obrigatorio aqui, apesar de ser opcional nos botoes.
function enviarLista(titulo, texto, rodape, textoBotao, secoes) {
return enviar("sendList", {
number: DESTINO,
title: titulo,
description: texto,
footerText: rodape,
buttonText: textoBotao,
sections: secoes
});
}
// De 2 a 10 opcoes. selectableCount = 1 e voto unico.
function enviarEnquete(pergunta, opcoes, quantosVotos) {
return enviar("sendPoll", {
number: DESTINO,
name: pergunta,
selectableCount: quantosVotos,
values: opcoes
});
}
E o main(), que escolhe o tipo pelo argumento da linha de comando. Um if atrás do outro, de propósito: dá para ler de cima a baixo sem parar para decifrar nada.
async function main() {
const tipo = process.argv[2] || "texto";
try {
if (tipo === "texto") {
await enviarTexto("Ola! Mensagem de texto simples.");
} else if (tipo === "imagem") {
await enviarImagem("imagem.jpg", "Uma imagem com legenda");
} else if (tipo === "audio") {
await enviarAudio("audio.ogg");
} else if (tipo === "video") {
await enviarVideo("video.mp4", "Um video com legenda");
} else if (tipo === "localizacao") {
await enviarLocalizacao("Praca Central", "Rua das Flores, 100",
-26.3044, -48.8487);
} else if (tipo === "contato") {
await enviarContato("Suporte da Loja", "5547999990000",
"Loja Exemplo");
} else if (tipo === "botoes") {
await enviarBotoes("Pedido #1234", "Confirma o seu pedido?",
"Loja Exemplo", [
{ type: "reply", displayText: "Confirmar", id: "sim" },
{ type: "reply", displayText: "Cancelar", id: "nao" }
]);
} else if (tipo === "enquete") {
await enviarEnquete("Qual sabor voce prefere?",
["Chocolate", "Morango", "Baunilha"], 1);
} else {
console.log("Tipo desconhecido: " + tipo);
}
} catch (erro) {
console.log("Falhou ao falar com a API: " + erro.message);
}
}
main();
O try/catch ali cobre um caso específico e nada mais: o IP errado, o servidor fora do ar, a rede caindo. Nesses casos o fetch não devolve resposta, ele estoura, e sem o catch você veria um rastro de pilha em vez de uma frase. Erro de validação da API não passa por ali, porque ele é uma resposta: quem trata é o if do status dentro da enviar().
Para rodar, é uma linha por tipo. Ponha a sua chave e o seu IP nas variáveis de ambiente, e chame:
export EVO_URL="http://SEU_IP_AQUI:8080"
export EVO_KEY="SUA_CHAVE_AQUI"
export EVO_DESTINO="SEU_NUMERO_AQUI"
node enviar.js texto
node enviar.js imagem
node enviar.js audio
node enviar.js video
node enviar.js localizacao
node enviar.js contato
node enviar.js botoes
node enviar.js enquete
E a saída, com os oito tipos que o WhatsApp entrega:
ok sendText id=3EB01470519A97AD59A70A
ok sendMedia id=3EB020A780F38D4D4660DD
ok sendWhatsAppAudio id=3EB0CE6476C0AF26996F8A
ok sendMedia id=3EB0ECB86F4CE3BE18BB42
ok sendLocation id=3EB0E89CB95E555C96E247
ok sendContact id=3EB058C2117B2E832E63ED
ok sendButtons id=3EB0104E30EDCAC370A77C
ok sendPoll id=3EB09824169010FA6BDB13
Aquele id impresso em cada linha é o que fecha o ciclo do artigo: é ele que você procura no log quando uma mensagem não aparecer no celular. Sem guardá-lo, você fica sem meio de saber se o problema foi seu ou do WhatsApp.
🎒 Os campos que aparecem em todos os tipos
Uma última coisa que economiza tempo: há campos opcionais que funcionam em qualquer uma das nove rotas, porque são tratados antes do tipo da mensagem.
Campo O que faz
─────────────── ─────────────────────────────────────────
delay espera N milissegundos antes de enviar
quoted responde citando outra mensagem
mentionsEveryOne marca todos os participantes do grupo
mentioned marca contatos especificos
O delay é o mais útil dos quatro no dia a dia. Ele é o jeito mais simples de não disparar dez mensagens no mesmo segundo, o que é justamente o padrão de robô que aumenta o risco do seu número. Um delay de alguns segundos entre envios custa nada e parece bem mais com uma pessoa digitando.
Em inglês, na documentação, os campos aparecem como delay, quoted, mentionsEveryOne e mentioned, dentro do mesmo objeto do corpo da requisição.
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Perguntas frequentes
Qual a rota para enviar cada tipo de mensagem na Evolution API?
sendText para texto, sendMedia para imagem e vídeo (mudando o campo mediatype), sendWhatsAppAudio para áudio de voz, sendLocation para localização, sendContact para contato, sendButtons para botões, sendList para listas e sendPoll para enquetes. Todas são POST em /message/<rota>/<instancia>, com a apikey no cabeçalho.Por que a Evolution API responde 200 e a mensagem não chega?
messages.update com status: ERROR e o mesmo id que a resposta devolveu. Sem olhar esse evento (ou o log da Evolution), o envio parece ter dado certo.Quantos botões a Evolution API aceita numa mensagem?
400 e a mensagem Maximum of 3 reply buttons allowed. Além do tipo reply, existem os botões url (abre um link), call (liga para um número) e copy (copia um código), que usam campos próprios em vez do id.Quantas opções pode ter uma enquete no WhatsApp?
values does not meet minimum length of 2; com onze ou mais, values does not meet maximum length of 10. O campo selectableCount define quantas alternativas cada pessoa pode marcar: 1 é voto único.Qual a diferença entre sendMedia com áudio e sendWhatsAppAudio?
sendWhatsAppAudio manda o áudio como mensagem de voz, aquele balão com a onda sonora e o botão de tocar. O sendMedia com mediatype: audio manda como arquivo anexado, que o WhatsApp mostra como documento. Para o áudio parecer gravado na hora, é o sendWhatsAppAudio.Como mandar uma imagem sem hospedar o arquivo em algum lugar?
media, sem o prefixo data:image/jpeg;base64,. Vale lembrar que, quando você passa uma URL nesse campo, quem baixa o arquivo é o servidor da Evolution: se o site responder 403 para ele, a chamada devolve 500 e a mensagem se perde.Leia também
Webhook da Evolution API: receber e saber se foi lida
Configure o webhook da Evolution API para saber quando chega mensagem, quando ela é lida e quando a conexão cai. Com os JSONs de ida e de volta.
WhatsApp: enviar mensagens grátis com a Evolution API
A Meta passa a cobrar por mensagem em 1/10/2026. Como instalar a Evolution API numa VPS com Docker e enviar mensagens no WhatsApp sem pagar por envio.