Pular para o conteúdo principal

Enviar mensagens

Existe um endpoint de envio: POST /messages. O que muda entre um texto e um template é o campo type no corpo — não a URL.

typePara quê
textTexto livre. Só dentro da janela de 24 horas
templateTemplate aprovado. Abre conversa a qualquer momento
testIgual a template, com o hello_world por padrão e erros traduzidos

Todos aceitam os mesmos campos base:

CampoObrigatórioO que faz
typesimtext, template ou test.
tosimNúmero do destinatário em formato internacional, só dígitos.
fromnãophone_number_id de origem. Omitido, usa o número ativo mais antigo.
contextMessageIdnãowamid de uma mensagem recebida — sua mensagem aparece citando ela.
sendAtnãoInstante ISO 8601 para enviar depois. Ver Agendar.
delaySecondsnãoMesma coisa, em segundos a partir de agora.

A resposta 201 traz o WAMID, o identificador que casa a mensagem com os eventos de status que chegam depois por webhook.

{
"data": {
"messages": [{ "id": "wamid.HBgNNTU0MTk4NzExMTUyNxUCABEYEjhF" }],
"from": "106540352242922",
"to": "5541987111527"
}
}
Texto livre exige a janela aberta

Só templates funcionam fora das 24 horas. Veja A janela de 24 horas.

Texto​

curl -X POST https://api.joinotify.com/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"type": "text",
"to": "5541987111527",
"body": "Seu pedido #1042 saiu para entrega.",
"previewUrl": true
}'

Até 4096 caracteres. previewUrl: true gera o cartão de pré-visualização do primeiro link — a Meta precisa conseguir acessar a URL publicamente para montar o cartão.

Template​

A única forma de escrever fora da janela de 24 horas.

curl -X POST https://api.joinotify.com/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"type": "template",
"to": "5541987111527",
"name": "confirmacao_pedido",
"language": "pt_BR",
"components": [
{ "type": "body", "parameters": [
{ "type": "text", "text": "Maria" },
{ "type": "text", "text": "1042" }
]}
]
}'

A ordem dos parameters segue a numeração das variáveis ({{1}}, {{2}}). Template sem variáveis dispensa components. Detalhes em Templates.

Teste​

Para responder "meu número está conectado e envia?" sem montar nada:

curl -X POST https://api.joinotify.com/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "type": "test", "to": "5541987111527" }'

O padrão é o hello_world, que sai sem variável.

hello_world só sai de número de teste

De um número de produção a Meta recusa com (#131058). É regra de origem do número — token e payload não resolvem. Nesse caso, informe name, language e components de um template aprovado seu; a API traduz esse erro (type: "test_number_only") dizendo exatamente isso.

Agendar​

Os mesmos envios aceitam sendAt (instante absoluto) ou delaySeconds (relativo). Os dois existem porque os dois casos existem: "sexta às 9h" é uma data, "cinco minutos depois do carrinho abandonado" é um atraso — e converter um no outro é onde entram os erros de fuso horário.

Daqui a duas horas
curl -X POST https://api.joinotify.com/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"type": "text",
"to": "5541987111527",
"body": "Seu carrinho ainda está separado 🙂",
"delaySeconds": 7200
}'

A resposta é 202, não 201 — aceitamos o pedido, mas nada foi criado na Meta ainda:

{
"data": {
"id": "clz3k1p9x0000",
"status": "pending",
"from": "106540352242922",
"to": "5541987111527",
"sendAt": "2026-08-15T21:00:00.000Z",
"messageId": null
}
}

Listar, consultar e cancelar:

curl https://api.joinotify.com/messages/scheduled?status=pending \
-H 'Authorization: Bearer sk_live_xxx'

curl -X DELETE https://api.joinotify.com/messages/scheduled/clz3k1p9x0000 \
-H 'Authorization: Bearer sk_live_xxx'

O cancelamento vale mesmo faltando segundos: quem entrega relê o registro antes de sair. Uma mensagem já enviada responde 404 — não há o que cancelar.

O que é reconferido na hora de enviar

Três coisas, e nenhuma delas no momento do agendamento: o direito de uso (uma assinatura que caiu no meio do caminho impede o envio), o número (você pode ter desconectado a conta) e o cancelamento. Um bloqueio por cobrança não é retentado: a mensagem falha com o motivo em failureReason e fica no histórico.

Repetir sem duplicar​

A conexão caiu depois que a Meta aceitou a mensagem, e a sua integração nunca viu a resposta. Repetir a chamada, sem mais nada, é uma segunda mensagem no WhatsApp da mesma pessoa — um código enviado duas vezes, uma promoção entregue em dobro.

O header Idempotency-Key resolve isso. Gere um valor por operação — o número do pedido e o evento, por exemplo — e mande o mesmo valor em todas as tentativas:

curl -X POST https://api.joinotify.com/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: pedido-1042-confirmacao' \
-d '{
"type": "text",
"to": "5541987111527",
"body": "Pedido 1042 confirmado ✅"
}'
SituaçãoResposta
Primeira chamadaA resposta normal
Mesma chave, mesmo corpoA primeira resposta, com Idempotent-Replayed: true — nada é enviado
Mesma chave, corpo diferente422 idempotency_key_reused
Mesma chave, primeira ainda rodando409 idempotency_in_progress — espere alguns segundos e repita

O que vale saber antes de confiar nisso:

  • A chave tem de 8 a 128 caracteres: letras, dígitos, _, -, . e :.
  • Ela vale por 24 horas e é da conta, não da API key: duas integrações da mesma conta que usem o mesmo valor se encontram.
  • Toda resposta final fica guardada, inclusive os erros. Um 502 porque a Meta não respondeu pode ter sido entregue mesmo assim, e a repetição devolve o mesmo 502 em vez de arriscar mandar de novo. Para tentar de novo de verdade, use uma chave nova — sabendo que pode duplicar.
  • O corpo é comparado como chegou: serialize o JSON sempre do mesmo jeito, ou a mesma mensagem com os campos em outra ordem vira "outro corpo".
  • Vale também para agendamentos: repetir um sendAt com a mesma chave não cria um segundo agendamento.

Os outros tipos de mensagem​

Imagem, vídeo, áudio, documento, figurinha, localização, contato, reação, botões, lista, CTA e carrossel não têm atalho de alto nível — e não precisam ter. O espelho POST /v1/{phone_number_id}/messages aceita o payload da Cloud API inteiro, com qualquer tipo que a Meta suporte, usando o seu sk_live_:

Imagem por URL
curl -X POST https://api.joinotify.com/v1/106540352242922/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "5541987111527",
"type": "image",
"image": { "link": "https://exemplo.com/catalogo.jpg", "caption": "Catálogo de julho" }
}'
Botões de resposta rápida
curl -X POST https://api.joinotify.com/v1/106540352242922/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "5541987111527",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Podemos entregar amanhã entre 9h e 12h?" },
"action": { "buttons": [
{ "type": "reply", "reply": { "id": "confirma", "title": "Pode sim" } },
{ "type": "reply", "reply": { "id": "remarcar", "title": "Quero remarcar" } }
]}
}
}'
Marcar como lida
curl -X POST https://api.joinotify.com/v1/106540352242922/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgNNTU0MTk4NzExMTUyNxUCABEYEjhF"
}'

O formato de cada tipo é o da documentação da Meta, sem tradução nossa no meio — é a vantagem do espelho: o que vale lá vale aqui.

Quando o mesmo arquivo vai para muita gente, suba a mídia uma vez com POST /v1/{phone_number_id}/media e reutilize o id. Com link, a Meta baixa o arquivo do seu servidor a cada envio. Veja Mídia.

Quando o cliente toca num botão, chega um webhook messages com type: "interactive":

{
"type": "interactive",
"interactive": {
"type": "button_reply",
"button_reply": { "id": "confirma", "title": "Pode sim" }
}
}

Trate o id, não o title. O título é texto de interface e pode mudar; o id é o contrato.

Acompanhando a entrega​

Nenhum envio confirma a entrega — o 201 diz apenas que o WhatsApp aceitou a mensagem. O que acontece depois chega por webhook messages, no array statuses:

statusSignificado
sentSaiu da infraestrutura da Meta
deliveredChegou ao aparelho
readFoi aberta (se o cliente não desativou a confirmação)
failedNão foi entregue — o motivo vem em errors[]

Guarde o WAMID do 201 e case com o id do status. Veja Webhooks.

Se você não quiser manter estado do seu lado — ou estiver depurando um envio isolado — consulte o último status conhecido de um WAMID:

curl https://api.joinotify.com/messages/status/wamid.HBgNNTU0MTk4NzExMTUyNxUCABEYEjhF \
-H 'Authorization: Bearer sk_live_xxx'
{
"data": {
"messageId": "wamid.HBgNNTU0MTk4NzExMTUyNxUCABEYEjhF",
"status": "failed",
"recipientId": "5541987111527",
"errors": [{ "code": 131047, "title": "Re-engagement message" }],
"updatedAt": "2026-07-29T01:51:43.030Z"
}
}

A resposta vem do mesmo espelho que alimenta Eventos recebidos — não custa chamada na Meta nem cota de envio. status: null significa que nenhum status chegou ainda (costuma levar segundos) ou que os webhooks da conta não estão sendo entregues.

Aceito não é entregue

Um texto livre fora da janela de 24 horas é aceito (você recebe 201 e um WAMID) e reprovado alguns segundos depois, com o código 131047 no status failed. Se a sua integração trata o 201 como sucesso final, ela vai jurar que enviou uma mensagem que nunca chegou. Ver Janela de 24 horas.