Envio de mensagens
Referência campo a campo: /docs#operation/sendMessage.
O formato do corpo
Todos os campos ficam na raiz do objeto. to e message são obrigatórios sempre, inclusive
ao enviar mídia.
{
"to": "5511123456789",
"message": "Olá! Identificamos um débito em aberto. Podemos conversar?"
}
⚠️ Se você encontrou um exemplo com
content: { text: ... }, ele está errado. Versões antigas da nossa documentação ensinavam esse formato aninhado, que o servidor nunca aceitou. Ele devolve400combody must have required property 'message'. Os exemplos deste guia são verificados automaticamente contra o servidor real justamente por causa desse episódio.
| Campo | Obrigatório | Descrição |
|---|---|---|
to |
sim | Número de destino (10 a 15 dígitos, só números) ou id de um grupo (...@g.us) |
message |
sim | Texto (1 a 4096 caracteres). Em imagens, vira a legenda só quando o pedido não traz caption |
type |
não | text (padrão), image ou document |
mediaUrl |
para mídia | Link https:// do arquivo ou data URL base64 (data:<mime>;base64,...), até 5 MB |
filename |
não | Nome do arquivo, para documentos |
caption |
não | Legenda da mídia. Em imagem, "" envia sem legenda |
Enviando mídia
O arquivo pode ir de dois jeitos em mediaUrl, com até 5 MB:
- Link
https://: o DevZap baixa o arquivo na hora, antes do envio, e usa o tipo que o servidor do link informa noContent-Type. É o mesmo download da fila. - Dentro do próprio JSON, como data URL em base64:
data:<mime>;base64,<conteúdo>. Acima de 5 MB a resposta é400comVALIDATION_ERROR.
Quando o link não baixa, a resposta é 400 e nada é enviado:
| Código | Quando |
|---|---|
MEDIA_DOWNLOAD_FAILED |
O servidor do link respondeu erro (o status vem na mensagem) ou não respondeu em 15 s |
MEDIA_URL_BLOCKED |
O link aponta para um endereço interno ou o domínio não resolve |
MEDIA_TOO_LARGE |
O arquivo do link passa de 5 MB |
O link precisa ser acessível de fora, sem login: um link de painel que pede sessão devolve a página de login, não o arquivo.
Imagem:
{
"to": "5511123456789",
"message": "Segue o comprovante",
"type": "image",
"mediaUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...",
"caption": "Comprovante de pagamento"
}
Se o pedido traz caption, ela é a legenda, mesmo vazia. Sem o campo caption, o valor de
message vira a legenda.
Imagem sem legenda: mande caption vazia. O message continua obrigatório, mas não aparece
na imagem.
{
"to": "5511123456789",
"message": "Foto do produto",
"type": "image",
"mediaUrl": "https://exemplo.com.br/produtos/4711.jpg",
"caption": ""
}
Mudança de comportamento: antes,
"caption": ""era tratada como ausente e a imagem saía com o texto demessagecomo legenda.
Imagem por link:
{
"to": "5511123456789",
"message": "Segue o comprovante",
"type": "image",
"mediaUrl": "https://exemplo.com.br/comprovantes/4711.jpg",
"caption": "Comprovante de pagamento"
}
Documento:
{
"to": "5511123456789",
"message": "Sua nota fiscal",
"type": "document",
"mediaUrl": "data:application/pdf;base64,JVBERi0xLjQK...",
"filename": "nota-fiscal-4711.pdf"
}
O filename é o nome que o destinatário vê. Sem ele, o arquivo chega como document.
Enviando para grupos
- Liste os grupos do número com
GET /instances/{id}/groups. Cada item traz oiddo grupo (120363012345678901@g.us), o nome, o tamanho ecanSend. - Envie com o mesmo
/messages/send, pondo oiddo grupo emto, exatamente como veio. Texto, imagem e documento funcionam igual ao envio para um contato.
{
"to": "120363012345678901@g.us",
"message": "Relatório de contas a pagar",
"type": "document",
"mediaUrl": "data:application/pdf;base64,JVBERi0xLjQK...",
"filename": "contas-a-pagar.pdf",
"caption": "Relatório de contas a pagar"
}
Confira o canSend antes de oferecer o grupo ao seu usuário. Ele vem false quando só
administradores escrevem no grupo e o número não é admin, e na comunidade (o "pai"), que não
recebe mensagens. A fila e os agendamentos ainda não aceitam grupo: use o envio direto.
O envio para grupo conta nos mesmos limites do envio para contato. Criar grupos e adicionar pessoas não fazem parte da API.
Envio direto ou fila?
São dois endpoints com garantias diferentes.
Direto (/messages/send) |
Fila (/messages/queue/send) |
|
|---|---|---|
| Resposta | Depois do envio, com messageId |
Imediata, antes do envio |
| Retry automático | Não | Sim, com backoff |
| Sobrevive a restart | Não | Sim |
| Idempotência | Não | Sim, por idempotencyKey |
| Exige plano | Não | Sim (message_queue) |
| Disponibilidade | Sempre | Só com a fila habilitada no servidor |
A fila precisa estar habilitada no servidor. Desabilitada,
/messages/queue/sendresponde503com o códigoQUEUE_DISABLEDe nada é enfileirado. Nesse caso, use o envio direto.
Use o direto quando você precisa do resultado agora, um fluxo interativo em que o usuário está esperando, por exemplo.
Use a fila quando a entrega importa mais que a imediatez: notificações, cobranças, disparos em lote. E principalmente quando duplicar seria ruim.
Cuidado ao migrar de um para o outro
Os corpos são diferentes, e essa é uma armadilha fácil:
| Direto | Fila | |
|---|---|---|
type |
text, image, document |
text, media |
| Tipo do arquivo | vai no próprio type |
vai em mediaType |
idempotencyKey |
não existe | obrigatório |
Enviar type: "image" para a fila resulta em 400. Na fila, o certo é type: "media" com
mediaType: "image".
{
"to": "5511123456789",
"idempotencyKey": "pedido-4711-nota",
"type": "media",
"mediaUrl": "https://exemplo.com.br/nota.pdf",
"mediaType": "document",
"filename": "nota-fiscal-4711.pdf"
}
Lendo a resposta da fila
O código de status distingue os casos, e vale tratá-los:
- 202: enfileirada agora, ou ainda em processamento de uma chamada anterior.
- 200 com
cached: true: esta chave já foi processada e a mensagem já foi enviada. Nada novo foi enfileirado; a resposta traz o resultado original.
Ou seja: um 200 aqui significa "já fiz isso", não "acabei de fazer". Para acompanhar o
andamento, consulte GET /instances/{id}/messages/queue/{idempotencyKey}.
Quanto tempo a mensagem espera na fila
A mensagem vale por 1 hora a partir da entrada na fila. Se não sair nesse prazo (instância
desconectada, tentativas com erro), ela vira expired e não é mais enviada. A consulta de status
já mostra expired, com o motivo em lastError. A regra existe porque resposta de chatbot que
chega uma hora depois costuma ser pior que nenhuma.
Outros motivos para uma mensagem da fila não sair, todos com status: "failed" e o motivo em
lastError:
- Conta sem plano em dia na hora do envio. A fila envia depois da requisição, então a conta é conferida de novo quando a mensagem sai: plano vencido ou conta suspensa, ela não sai.
- Mídia acima de 5 MB. O arquivo de
mediaUrlé baixado com esse teto. Acima dele, a mensagem falha sem nova tentativa, porque baixar de novo não diminui o arquivo. - Tentativas esgotadas. Erro no envio gera novas tentativas com espera crescente; esgotadas, a mensagem falha.
Por quanto tempo a fila guarda a mensagem
Depois que a mensagem sai da fila (enviada, failed ou expired), o registro dela fica guardado
por 30 dias. Passado esse prazo ele é apagado: a consulta de status pela idempotencyKey
responde 404, e enfileirar de novo com a mesma chave cria uma mensagem nova. Se a sua integração
pode repetir um envio depois disso, monte a chave com algo que não se repete (o id do pedido, por
exemplo).
Consultando o histórico
GET /instances/{id}/messages devolve as mensagens da instância, mais recentes primeiro, com
paginação por limit/offset e dois filtros opcionais:
phone: só as mensagens trocadas com esse número;direction:outtraz só as enviadas,insó as recebidas. Sem ele, vêm as duas. Outro valor dá 400VALIDATION_ERROR.
Para buscar por conteúdo ou período, e em todas as instâncias, existe GET /messages/search: mas
ele depende da funcionalidade search_messages no plano. Veja
Limites e planos.
Boas práticas
Não envie em rajada. O WhatsApp tem seus próprios mecanismos de detecção de abuso, e eles são independentes dos nossos limites. Disparos em massa e rápidos podem levar ao banimento do número, um problema que nenhuma API resolve para você. Espace os envios e prefira a fila.
Trate messageId como a chave da mensagem. É por ele que você cruza o envio com os eventos
que chegam depois.
Cheque o estado antes de disparos grandes. Uma consulta a GET /instances/{id} evita
descobrir que a instância caiu só depois de 500 falhas.
Não retente cegamente. SEND_ERROR e VALIDATION_ERROR não melhoram com repetição. Veja
Erros e retry.
Guia verificado em 05/08/2026 contra a API v1. Os blocos marcados com
<!-- fixture: ... -->são comparados automaticamente com os arquivos de contrato em cada build.