Documentação

API Oficial do WhatsApp

Como ligar o número à Plataforma da Meta, enviar mensagens e modelos, e receber os eventos no seu sistema.

Atualizado a 12 de novembro de 2026

Índice

Visão geral

A API Oficial liga o seu número de WhatsApp à Plataforma da Meta (WhatsApp Cloud API) e dá-lhe uma API simples para enviar mensagens e receber o que os seus clientes escrevem. Funciona com o n8n, com o seu CRM ou com código seu.

O que recebe:

  • uma chave de acesso para enviar mensagens, modelos e consultar o estado dos números;

  • um webhook: cada mensagem recebida e cada mudança de estado chegam ao endereço que indicar, assinadas;

  • o portal do cliente, onde liga o número, cria modelos de mensagem e acompanha o consumo.

Todos os pedidos vão para:

https://wa.automate-ai.digital

As mensagens são cobradas pela Meta, diretamente à sua conta (ver Custos). O plano cobre o número e a API.

Começar

1. Ligar o número

No portal, em Meus produtos, carregue em Ligar número oficial e siga o fluxo da Meta: escolhe ou cria a conta WhatsApp Business, confirma o número e autoriza a ligação. No fim, o número aparece como ligado.

Duas coisas que a Meta pede:

  • a verificação da empresa na Meta, para sair dos limites iniciais de envio;

  • um método de pagamento na conta WhatsApp Business: sem ele, a Meta deixa de enviar mensagens.

2. Configurar o webhook e a chave

No portal, em Integração técnica:

  1. Webhook: indique o endereço que vai receber os eventos. O portal mostra uma vez o segredo do webhook, com o qual se verifica a assinatura. Guarde-o.

  2. Chave de acesso: gere a chave. Também aparece uma única vez.

3. Usar o fluxo n8n pronto

Se usa n8n, no mesmo sítio há o botão Descarregar fluxo n8n. O fluxo já verifica a assinatura, responde às mensagens e envia modelos. Importe-o, ative-o, cole o URL de produção dele no passo Webhook, e cole o segredo e a chave onde o fluxo indica.

Autenticação

Todos os pedidos levam a chave no cabeçalho x-api-key:

curl https://wa.automate-ai.digital/instances/ID_DO_NUMERO \
  -H "x-api-key: wak_xxxxxxxx_xxxxxxxx"

Todas as respostas têm a mesma forma:

{ "ok": true, "data": { "...": "..." } }
{ "ok": false, "error": { "code": "INSTANCE_NOT_FOUND", "message": "…", "retryable": false } }

O ID_DO_NUMERO (instanceId) vem em cada evento que recebe no webhook.

Uma chave nova invalida a anterior no momento em que é gerada. Tudo o que usava a antiga deixa de funcionar até ser atualizado.

Enviar mensagens

curl -X POST https://wa.automate-ai.digital/instances/ID_DO_NUMERO/messages \
  -H "x-api-key: $CHAVE" -H "content-type: application/json" \
  -d '{
    "to": { "kind": "contact", "id": "351912345678" },
    "content": { "type": "text", "body": "Olá! Já tratámos do seu pedido." },
    "idempotencyKey": "pedido-1234-confirmacao"
  }'
  • to.id é o número em formato internacional, sem + nem espaços.

  • idempotencyKey (opcional, recomendado): se o mesmo pedido for repetido com a mesma chave, a mensagem não sai duas vezes.

  • replyTo (opcional): o providerMessageId da mensagem a que responde, para aparecer como citação.

A resposta é 202 com o id da mensagem:

{ "ok": true, "data": { "messageId": "44444444-…", "queuedAt": "2026-11-12T10:00:00.000Z" } }

202 quer dizer que a mensagem entrou na fila, não que foi entregue. O desfecho chega depois no webhook, como message.status (ver Estado dos envios).

Tipos de conteúdo

type

Campos

text

body (até 4096 caracteres)

image

media, caption

video

media, caption

audio

media

document

media, filename, caption

location

latitude, longitude, name, address

buttons

body, buttons: até 3, cada um com id e title (até 20 caracteres)

media é { "kind": "url", "url": "https://…" }, ou { "kind": "base64", "data": "…", "mimeType": "image/png" }.

Botões de resposta

{
  "to": { "kind": "contact", "id": "351912345678" },
  "content": {
    "type": "buttons",
    "body": "Confirma a marcação de amanhã às 15h?",
    "buttons": [
      { "id": "confirmar", "title": "Confirmar" },
      { "id": "remarcar", "title": "Remarcar" }
    ]
  }
}

Quando o cliente carrega num botão, recebe um message.received com content.type igual a interactive_reply e o mesmo id em replyId. É por ele que o seu fluxo decide o que fazer.

Janela de 24 horas e modelos

É a regra mais importante do canal oficial:

  • dentro de 24 horas desde a última mensagem do cliente, pode enviar mensagens livres (/messages) e modelos;

  • fora dessa janela, só modelos aprovados pela Meta (/messages/template).

Uma resposta a uma mensagem que acabou de receber está sempre dentro da janela. Começar uma conversa (lembrete, confirmação, aviso) é sempre com modelo.

Cada message.received traz windowExpiresAt, o momento em que a janela fecha.

Uma mensagem livre fora da janela é aceite com 202 e falha depois, no webhook, com failureCode igual a WINDOW_CLOSED. A API não troca a mensagem por um modelo sozinha.

Enviar um modelo

curl -X POST https://wa.automate-ai.digital/instances/ID_DO_NUMERO/messages/template \
  -H "x-api-key: $CHAVE" -H "content-type: application/json" \
  -d '{
    "to": { "kind": "contact", "id": "351912345678" },
    "templateName": "pedido_confirmado",
    "languageCode": "pt_PT",
    "bodyParameters": ["Maria", "A-12"],
    "buttonParameters": [{ "type": "url", "index": 1, "text": "A-12" }],
    "idempotencyKey": "pedido-A-12-confirmado"
  }'

Campo

Para quê

templateName

o nome do modelo, como aparece no portal

languageCode

o idioma do modelo (pt_PT, pt_BR, en, fr…)

bodyParameters

os valores de {{1}}, {{2}}… do texto, por ordem

headerParameter

o valor da variável do cabeçalho: texto, ou imagem { "kind": "url", "url": "…" }

buttonParameters

o valor de cada botão de link com parte variável

Botão de link com parte variável. Num modelo com o link https://loja.pt/pedido/{{1}}, cada envio tem de dizer o que entra no {{1}}. index é a posição do botão no modelo, a contar de 0 e contando todos os botões (respostas rápidas e telefone incluídos). text é só a parte variável (A-12), nunca o link inteiro. Sem este valor, a Meta recusa o envio.

Criar modelos

No portal, no número oficial, separador Modelos, carregue em Criar modelo. São quatro passos: nome e categoria, texto com variáveis e exemplos, botões, e rever. A Meta analisa cada modelo, normalmente entre minutos e algumas horas.

  • Utilidade: avisos sobre algo que o cliente pediu (pedido, marcação, pagamento). É aprovado mais depressa.

  • Marketing: promoções e novidades.

Modelos com imagem no cabeçalho, carrossel ou autenticação criam-se no WhatsApp Manager da Meta. O portal tem um atalho para lá.

Quando a Meta decide, recebe um evento template.status (ver Eventos). Só um modelo com estado approved pode ser enviado.

Eventos

Cada evento chega ao seu webhook num POST com o corpo em JSON. Há quatro tipos:

type

Quando

message.received

o cliente escreveu

message.status

um envio mudou de estado: sent, held, delivered, read, failed

connection.updated

o estado do número mudou

template.status

um modelo foi aprovado, recusado, pausado…

Ignore os tipos que não conhece, respondendo 2xx. A lista vai crescer, e um fluxo que assume "se não é A, então é B" trata um tipo novo como aquilo que ele não é.

message.received

{
  "type": "message.received",
  "eventId": "0f0f6ab7-2a2f-4f0b-9b6a-9a5f7d5d31c2",
  "instanceId": "0d9b1b6f-6a2a-4a4c-9f66-3f6a3f0b6a2a",
  "provider": "cloud_api",
  "occurredAt": "2026-11-12T18:41:07.000Z",
  "providerMessageId": "wamid.HBgM…",
  "from": "351912345678",
  "pushName": "Maria",
  "content": { "type": "text", "body": "Olá, queria saber do meu pedido" },
  "windowExpiresAt": "2026-11-13T18:41:07.000Z"
}

Tipos de content:

type

Campos

text

body

media

mediaType (image, video, audio, document, sticker), storageUrl, storagePath, mimeType, filename, caption

location

latitude, longitude, name

interactive_reply

replyId, title: a resposta a um botão

reaction

emoji, targetMessageId

unsupported

providerType: um formato que a API ainda não lê

O storageUrl da mídia é temporário: serve para descarregar o ficheiro logo, não para guardar.

Se o seu número tiver o agrupamento de mensagens ligado, textos seguidos do mesmo cliente chegam num só evento, unidos por quebras de linha. Esse evento traz todos os ids em providerMessageIds.

template.status

{
  "type": "template.status",
  "instanceId": "0d9b1b6f-…",
  "template": { "id": "806312974732579", "name": "pedido_confirmado", "language": "pt_PT", "category": "utility" },
  "status": "rejected",
  "reason": "invalid_format",
  "rejection": { "reason": "…", "recommendation": "…" }
}

status vem em minúsculas: pending, approved, rejected, paused, disabled, entre outros. A Meta acrescenta estados sem aviso: trate os que lhe interessam e ignore o resto. Uma conta com vários números recebe um aviso por número, e deduplica-se por template.id com status.

connection.updated

Traz status (connected, disconnected, logged_out…) e requiresReauth. Com requiresReauth: true, o número precisa de ser ligado outra vez no portal: a autorização da Meta deixou de valer.

Verificar a assinatura

Cada pedido do webhook traz:

x-timestamp: 1794519022
x-signature: sha256=9c1f…

A assinatura é um HMAC-SHA256, com o segredo do webhook, sobre x-timestamp + "." + corpo cru. Para verificar:

  1. recuse o pedido se faltar x-signature ou x-timestamp;

  2. recuse se x-timestamp tiver mais de 5 minutos de diferença do relógio;

  3. calcule o HMAC sobre o corpo cru, byte a byte. Se voltar a serializar o JSON, a assinatura falha em alguns eventos;

  4. compare com uma comparação de tempo constante.

const crypto = require("crypto");

function assinaturaValida(segredo, timestamp, corpoCru, assinatura, assinaturaNova) {
  if (!assinatura || !timestamp) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const esperado = Buffer.from(
    "sha256=" + crypto.createHmac("sha256", segredo).update(`${timestamp}.${corpoCru}`).digest("hex")
  );
  const bate = (h) => !!h && Buffer.from(h).length === esperado.length && crypto.timingSafeEqual(Buffer.from(h), esperado);
  return bate(assinatura) || bate(assinaturaNova);
}

Verifique antes de responder. Se a assinatura falhar, responda 401: o evento fica guardado do nosso lado e não se perde. Se responder 2xx antes de verificar, um evento recusado depois perde-se sem rasto.

Troca do segredo sem perder eventos. Durante uma troca, cada pedido traz também x-signature-next, assinado com o segredo novo. Aceite qualquer uma das duas assinaturas. Assim pode passar a usar o segredo novo quando quiser, sem uma janela em que os eventos falham.

Estado dos envios

status

Quer dizer

sent

a Meta aceitou a mensagem

held

a Meta aceitou e reteve a mensagem para avaliar a qualidade. Ainda não é sucesso nem falha

delivered

chegou ao telemóvel do cliente

read

o cliente leu

failed

não saiu: failureCode diz porquê e failureReason traz o texto da Meta

Com held, espere pelo estado seguinte antes de confirmar o que quer que seja ao cliente.

Ligar o estado à mensagem

Os eventos de estado trazem messageId (o id que a API lhe devolveu), providerMessageId (o id da Meta), ou os dois. Nunca chegam sem nenhum.

POST /messages            → { messageId: "4444…" }
message.status sent       → { messageId: "4444…", providerMessageId: "wamid.X" }   ← o par
message.status delivered  → { providerMessageId: "wamid.X" }
message.status read       → { providerMessageId: "wamid.X" }

Guarde o providerMessageId que chega com o sent (ou o held). Os delivered e read seguintes trazem só esse.

Erros

Na resposta do pedido. São erros que se resolvem no próprio pedido:

HTTP

code

Quando

400

INVALID_PAYLOAD

o corpo não está certo (a mensagem diz o quê)

401

UNAUTHORIZED

chave em falta, errada ou revogada

404

INSTANCE_NOT_FOUND

o número não existe na sua conta

No webhook, num message.status de failed. Tudo o que depende da Meta chega aqui, depois do 202:

failureCode

Quando

Onde ver

WINDOW_CLOSED

mensagem livre fora da janela de 24 horas

envie um modelo

TEMPLATE_NOT_APPROVED

o modelo não existe nesse idioma, não está aprovado, ou foi pausado

separador Modelos no portal

INVALID_PAYLOAD

o número de variáveis não bate com o modelo, ou o texto ficou longo demais

o corpo do pedido

CAPABILITY_UNAVAILABLE

algo que este canal não faz (por exemplo, envio para grupo)

Limites

INSTANCE_NOT_CONNECTED

o número não está ligado

portal

PROVIDER_REJECTED

a Meta recusou por outro motivo

o texto em failureReason

Entrega dos eventos

  • Responda 2xx logo que receber, antes de processar. Cada tentativa tem 10 segundos. Um endpoint que responde só no fim do fluxo arrisca receber o mesmo evento outra vez.

  • Deduplique por eventId. Cada evento é entregue pelo menos uma vez e pode chegar repetido.

  • Ordene por occurredAt, não pela ordem de chegada. Um delivered pode chegar antes do sent.

  • Repetições: com um 5xx, um 429 ou sem resposta, repetimos até 8 vezes ao longo de cerca de 30 minutos. Um 4xx (exceto 429) não é repetido: o evento fica guardado do nosso lado.

Limites do canal

O canal oficial não tem:

  • grupos: não envia nem recebe mensagens de grupos;

  • indicador "a escrever" nem apagar mensagens enviadas;

  • nota de voz como opção: a Meta decide pelo formato do ficheiro (um áudio ogg/opus aparece como nota de voz);

  • cabeçalho de modelo em base64: use uma imagem por URL.

Pedidos destes falham com CAPABILITY_UNAVAILABLE, nunca em silêncio.

Custos

  • O plano (Essencial, Negócio ou Escala) cobre os números e a API, sem limite de mensagens da nossa parte.

  • As mensagens são cobradas pela Meta, por mensagem entregue, à sua conta WhatsApp Business. O preço depende da categoria (marketing, utilidade, autenticação) e do país do cliente.

  • As respostas dentro da janela de 24 horas são mensagens de serviço, gratuitas até ao limite mensal que a Meta define.

  • Sem método de pagamento na conta da Meta, a Meta deixa de enviar mensagens, incluindo as respostas dentro da janela.

Os preços em vigor estão na página de preços da Plataforma WhatsApp da Meta.