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).
Enviar uma mensagem
Quais estados do canal aceitam envio:
| Status do canal | Resultado |
|---|---|
connected | 202, status: "sent" — entregue ao WhatsApp na hora. |
connecting / reconnecting / disconnected, ou pending / qr durante um novo pareamento de um canal que já foi pareado | 202, status: "queued" — fica guardada e sai assim que o canal conectar. |
pending / qr num canal que nunca foi pareado | 409 channel_unavailable com details.paired: false — escaneie o QR code antes. |
logged_out / stopped_by_user / disabled_by_admin / error / failed | 409 channel_unavailable — reconecte ou resete o canal antes. details.status diz em qual estado ele está. |
| Field | Type | Required | Notes |
|---|
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:
| Status | error.code | Quando |
|---|---|---|
| 400 | invalid_request | O corpo falhou na validação (details.issues diz o porquê). |
| 400 | undeliverable_recipient | O to não consegue receber mensagens de WhatsApp. details.to devolve o valor. |
| 402 | trial_expired / payment_overdue | Envios pausados na sua conta até você escolher um plano ou regularizar a cobrança. |
| 404 | channel_not_found | Não existe canal com esse id na sua conta. |
| 409 | channel_unavailable | O canal está em estado terminal (veja a tabela acima). |
| 429 | rate_limited | Vá mais devagar — veja Limites de requisições. |
| Field | Type | Required | Notes |
|---|---|---|---|
| message_id | string | required | |
| client_ref | string | required · nullable | |
| status | `sent` \| `queued` | required | |
| quoted | boolean | optional | |
| warnings | array<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 comopendingaté os servidores do WhatsApp confirmarem, depoissent→delivered→read."queued"— o canal estava em recuperação; seguramos a mensagem e retentamos sob o mesmomessage_idaté ela sair ou o canal parar em definitivo. Ela já aparece no histórico comopending. 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 webhookmessage.statuscomstatus: "failed"e umreasonlegível por máquina (veja payloads), e o histórico mostrafailed. Um envio para um canalconnectedtambé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 }quoted | Significado |
|---|---|
| ausente | A requisição não trouxe reply_to (ou voltou status: "queued" — veja abaixo). |
true | A mensagem saiu citando aquele id. |
false | O 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
GETpor id de mensagem — encontre ela nas rotas de histórico acima, ou acompanhe pelos webhooksmessage.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.