POST /messages
Uma chamada para todo tipo de mensagem: texto, template, mídia, botões, lista, link, pedido de localização e catálogo. Em vez de decorar nove endpoints diferentes, você muda o campo type.
Escopo necessário: messages:send. Exige plano com o recurso de API. Limite: 120 requisições por minuto.
O básico
curl -X POST "https://api.wevi.chat/functions/v1/messages" \
-H "Authorization: Bearer wevi_SUA_CHAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-8412-confirmado" \
-d '{
"type": "text",
"to": "5511999998888",
"text": "Seu pedido saiu para entrega."
}'
const res = await fetch("https://api.wevi.chat/functions/v1/messages", {
method: "POST",
headers: {
Authorization: "Bearer wevi_SUA_CHAVE",
"Content-Type": "application/json",
"Idempotency-Key": `pedido-${pedido.id}-confirmado`,
},
body: JSON.stringify({
type: "text",
to: pedido.telefone,
text: "Seu pedido saiu para entrega.",
}),
});
const { id, wamid, status } = await res.json();
Resposta:
{
"id": "bcb8e0c6-de1b-48dd-8712-c381ca4b1b74",
"wamid": "wamid.HBgLNTUxMT...",
"status": "sent",
"conversation_id": null,
"contact_id": null
}
Quando a Meta recusa, a resposta continua 200, com status: "failed" e o motivo em error. Recusa é resultado do envio, não erro da sua requisição: tratar como 5xx faz cliente com retry automático mandar a mensagem duas vezes.
Campos comuns
| Campo | Descrição |
|---|---|
type |
Obrigatório. Um dos tipos da tabela abaixo. |
to |
Telefone do destinatário, em qualquer formato brasileiro. |
conversation_id |
Alternativa a to: responde dentro de uma conversa existente. |
connection_id |
O número que envia. Pode omitir quando a organização só tem um. |
contact_id |
Associa o envio a um contato existente. |
contact |
{ "name": "...", "fields": { ... } }: cria ou atualiza o contato junto do envio. |
idempotency_key |
Também aceito no header Idempotency-Key. Retry com a mesma chave não reenvia. |
open_conversation |
Só para template: always (padrão) abre a conversa no inbox, on_reply espera o cliente responder. |
Tipos
type |
Campos próprios |
|---|---|
text |
text |
template |
template_name ou template_id, language_code, body_params, header_image_url, button_params |
image, audio, video, document |
media: { url, mime_type, name }, caption |
buttons |
body, buttons (até 3), header, footer |
list |
body, sections, button_text, header, footer |
cta_url |
body, url, display_text, header, footer |
location_request |
body |
product |
catalog_id, product_retailer_id, body, footer |
product_list |
catalog_id, sections, body, header, footer |
Template
{
"type": "template",
"to": "5511999998888",
"template_name": "confirmacao_pedido",
"language_code": "pt_BR",
"body_params": ["Maria", "8412"],
"open_conversation": "on_reply"
}
Texto livre só chega dentro da janela de 24 horas depois da última mensagem do cliente. Fora dela, use template: é a regra da Meta, não nossa.
Criar o contato junto do envio
{
"type": "template",
"to": "5511999998888",
"template_name": "boas_vindas",
"body_params": ["Maria"],
"contact": {
"name": "Maria Souza",
"fields": { "origem": "site", "plano": "premium" }
}
}
O contato é criado ou atualizado pelo telefone antes do envio, e o contact_id volta na resposta. Poupa uma chamada e garante que a conversa que nascer já esteja ligada à pessoa certa.
Botões
{
"type": "buttons",
"to": "5511999998888",
"body": "Podemos confirmar seu horário de quinta às 15h?",
"buttons": [
{ "id": "confirmar", "title": "Confirmar" },
{ "id": "remarcar", "title": "Remarcar" }
],
"footer": "Clínica OdontoVita"
}
Mídia
{
"type": "document",
"to": "5511999998888",
"media": {
"url": "https://seusistema.com/notas/8412.pdf",
"mime_type": "application/pdf",
"name": "nota-fiscal-8412.pdf"
},
"caption": "Segue a nota fiscal do seu pedido."
}
A URL precisa ser pública: quem baixa o arquivo é a Meta, não a gente.
Erros
| Status | error |
Quando |
|---|---|---|
| 400 | missing_type |
Faltou type. |
| 400 | unsupported_type |
type não existe. A resposta lista os aceitos em details.accepted. |
| 400 | missing_destination |
Nem to nem conversation_id. |
| 400 | missing_connection_id |
A organização tem mais de um número: diga qual usar. |
| 400 | no_connection |
Nenhum número de WhatsApp conectado. |
| 403 | opted_out |
O contato pediu para não receber mais e não escreveu nas últimas 24h. |
| 404 | connection_not_found |
O connection_id não é desta organização. |
| 200 | corpo com status: "failed" |
A Meta recusou. O motivo dela vem em error. |
E os endpoints antigos?
/send-message, /send-template, /send-buttons, /send-list, /send-cta-url, /send-location-request, /send-product, /send-product-list e /send-conversation-message continuam funcionando com o mesmo contrato de sempre. Por dentro, agora eles e o /messages usam a mesma implementação de envio.
Integração nova nasce melhor no /messages. Integração que já funciona não precisa mudar nada.
Próximo
/send-template