DevZap · Guias de integração Voltar ao painel

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 devolve 400 com body 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:

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 de message como 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

  1. Liste os grupos do número com GET /instances/{id}/groups. Cada item traz o id do grupo (120363012345678901@g.us), o nome, o tamanho e canSend.
  2. Envie com o mesmo /messages/send, pondo o id do grupo em to, 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/send responde 503 com o código QUEUE_DISABLED e 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:

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:

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:

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.


← Todos os guias