Skip to content
WhatIsUp.dev

Mensagens

Envie uma mensagem de WhatsApp a partir de um canal conectado. Mensagens recebidas chegam até você como webhooks message.received, e as duas direções também ficam guardadas e legíveis pelas rotas de histórico (veja Histórico de mensagens abaixo).

O envio é assíncrono — a API retorna na hora, o estado de entrega volta como um webhook.

Enviar uma mensagem

POST/v1/channels/:id/messagesBearer · API key

Quais estados do canal aceitam envio:

Status do canalResultado
connected202, status: "sent" — entregue ao WhatsApp na hora.
connecting / reconnecting / disconnected, ou pending / qr durante um novo pareamento de um canal que já foi pareado202, status: "queued" — fica guardada e sai assim que o canal conectar.
pending / qr num canal que nunca foi pareado409 channel_unavailable com details.paired: false — escaneie o QR code antes.
logged_out / stopped_by_user / disabled_by_admin / error / failed409 channel_unavailable — reconecte ou resete o canal antes. details.status diz em qual estado ele está.
Schema · Request body
FieldTypeRequiredNotes

O corpo é plano: type, to e os campos daquele tipo ficam lado a lado no nível de cima (não existe objeto body aninhado). Formatos válidos:

// text
{ "type": "text", "to": "5511999999999@s.whatsapp.net", "text": "hello" }
 
// image — mime_type é obrigatório para image / video / audio / document
{
  "type": "image",
  "to": "5511999999999@s.whatsapp.net",
  "media_url": "https://example.com/photo.jpg",
  "mime_type": "image/jpeg",
  "caption": "look"
}
 
// audio — ptt: true vira mensagem de voz (melhor com Ogg/Opus)
{
  "type": "audio",
  "to": "5511999999999@s.whatsapp.net",
  "media_url": "https://example.com/note.ogg",
  "mime_type": "audio/ogg; codecs=opus",
  "ptt": true
}
 
// document
{
  "type": "document",
  "to": "5511999999999@s.whatsapp.net",
  "media_url": "https://example.com/invoice.pdf",
  "mime_type": "application/pdf",
  "filename": "invoice.pdf"
}
 
// sticker (WebP — estático ou animado)
{
  "type": "sticker",
  "to": "5511999999999@s.whatsapp.net",
  "media_url": "https://example.com/sticker.webp"
}

Stickers precisam ser WebP. mime_type é opcional só para sticker e usa image/webp por padrão.

to é um JID do WhatsApp: o número internacional, só dígitos (sem +), seguido de @s.whatsapp.net para uma pessoa, ou o id de um grupo terminando em @g.us. Um número puro como "5511999999999" falha na validação com 400 invalid_request nesta rota. (O atalho tipado POST /v1/channels/:id/messages/:type aceita número puro e adiciona o sufixo pra você.) Um id @lid, ou um número que o WhatsApp diz não estar registrado, retorna 400 undeliverable_recipient.

curl -sX POST "$WHATISUP_API/v1/channels/8d653c66-e4ff-43ee-97da-3de5ad5680d4/messages" \
  -H "Authorization: Bearer $WHATISUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"text","to":"5511999999999@s.whatsapp.net","text":"hi"}'

Erros que esta rota retorna:

Statuserror.codeQuando
400invalid_requestO corpo falhou na validação (details.issues diz o porquê).
400undeliverable_recipientO to não consegue receber mensagens de WhatsApp. details.to devolve o valor.
402trial_expired / payment_overdueEnvios pausados na sua conta até você escolher um plano ou regularizar a cobrança.
404channel_not_foundNão existe canal com esse id na sua conta.
409channel_unavailableO canal está em estado terminal (veja a tabela acima).
429rate_limitedVá mais devagar — veja Limites de requisições.
Schema · Response
FieldTypeRequiredNotes
message_idstringrequired
client_refstringrequired · nullable
status`sent` \| `queued`required
quotedbooleanoptional
warningsarray<object>optional

A resposta é 202 Accepted, e status discrimina dois desfechos:

  • "sent" — o canal estava conectado e o gateway entregou a mensagem à conexão de WhatsApp dele. Isso é aceite, não entrega: no histórico a mensagem aparece como pending até os servidores do WhatsApp confirmarem, depois sent → delivered → read.
  • "queued" — o canal estava em recuperação; seguramos a mensagem e retentamos sob o mesmo message_id até ela sair ou o canal parar em definitivo. Ela já aparece no histórico como pending. Se não tiver como sair — o canal fica offline por mais de 2 horas, é desconectado ou parado, ou o destinatário não tem WhatsApp — você recebe um webhook message.status com status: "failed" e um reason legível por máquina (veja payloads), e o histórico mostra failed. Um envio para um canal connected também pode voltar "queued": enquanto mensagens mais antigas desse canal ainda esperam na fila, a nova entra atrás delas em vez de passar na frente.

Um array warnings aparece quando há algo a saber sobre um envio que saiu mesmo assim. Hoje: new_chat_restricted / new_chat_capped — o WhatsApp não está deixando este número começar conversas novas agora e o destinatário nunca escreveu pra ele, então o WhatsApp provavelmente vai recusar a mensagem (ela vira failed). O gateway nunca recusa o envio por conta própria; veja Mantendo os números saudáveis.

Os dois são sucesso. Refazer o POST depois de um "queued" cria uma segunda mensagem no WhatsApp — a não ser que as duas requisições levem o mesmo header Idempotency-Key; aí a retentativa recebe a resposta original (Idempotência).

O estado de entrega final chega em webhooks message.status carregando status: "sent" | "delivered" | "read" | "played" | "failed". Não existe evento message.delivered nem message.failed separado. Correlacione por message_id.

Responder uma mensagem (resposta citada)

Adicione reply_to a qualquer envio de mensagem (text, image, video, audio, document, sticker) e o WhatsApp renderiza a original acima da sua — igualzinho a arrastar para responder no app.

{
  "type": "text",
  "to": "5511999999999@s.whatsapp.net",
  "text": "Sábado abrimos às 9h.",
  "reply_to": { "message_id": "3EB0AA11BB22CC33DD44EE55FF66007788AA" }
}

reply_to.message_id é qualquer id de mensagem que este canal já viu: o message_id de um webhook message.received ou message.sent, o quoted.message_id de uma resposta recebida, ou o id devolvido por um envio anterior. Você nunca precisa guardar a chave interna do WhatsApp — o gateway reconstrói ela (conversa + autor) a partir do próprio armazenamento.

O 202 responde se a citação realmente foi:

{ "message_id": "3EB0…", "client_ref": null, "status": "sent", "quoted": true }
quotedSignificado
ausenteA requisição não trouxe reply_to (ou voltou status: "queued" — veja abaixo).
trueA mensagem saiu citando aquele id.
falseO id é desconhecido aqui — nunca visto neste canal, expirado pela retenção, ou de outra conversa. A mensagem foi enviada mesmo assim, só que sem a citação.

Um reply_to inválido nunca derruba um envio. Perder a citação tem conserto; perder a mensagem não tem. Se a citação importa no seu fluxo, confira o campo quoted da resposta em vez de presumir.

Em status: "queued" não há campo quoted: o canal estava offline, então a citação é resolvida depois, quando a mensagem enfileirada realmente sai. O webhook message.sent carrega o veredito no quoted dele.

Citar mensagem de outra conversa não é suportado pelo WhatsApp — o gateway descarta a citação (quoted: false) em vez de mandar uma chave que apareceria como citação quebrada no celular do destinatário.

Histórico de mensagens

GET /v1/messages (todos os canais) e GET /v1/channels/:id/messages (um canal) devolvem as mensagens de entrada e saída guardadas, mais recentes primeiro, com corpos, referências de mídia e as entregas de webhook que carregaram cada uma. Linhas de saída trazem o status de entrega (pending, sent, delivered, read, played ou failed) e status_updated_at, então dá pra ler o estado atual de uma mensagem aqui além de recebê-lo por webhooks message.status.

{
  "data": [ { "id": "…", "direction": "out", "status": "delivered", "…": "…" } ],
  "has_more": true,
  "next_cursor": "2026-09-25T12:00:00.000Z",
  "next_cursor_id": "0b8f…"
}

Pagine com limit (1–100, padrão 20) e, pra próxima página, mande o next_cursor da resposta anterior como before e o next_cursor_id como before_id. Quando uma requisição volta com data vazio, você chegou ao fim.

O que você não vê nesta API

  • Sem GET de uma mensagem só. Não existe GET por id de mensagem — encontre ela nas rotas de histórico acima, ou acompanhe pelos webhooks message.status.
  • Sem envio em massa. Faça loop na API; o limitador de requisições por cliente (Chaves de API → Limite de requisições) é o seu throttle. Batching estilo Stripe está no roadmap.
  • Sem "agendar envio". Agende do seu lado e nos chame na hora de disparar.

Emitimos message.status com status: "read" quando o WhatsApp informa que o destinatário abriu a conversa, e "played" para áudios e vídeos — ou seja, confirmação de leitura existe. A ressalva é a configuração de privacidade do destinatário: o WhatsApp suprime a confirmação por completo quando ele desliga confirmações de leitura, então a ausência de read não prova que a mensagem está não-lida.