---
title: WhatsApp Cloud API: como integrar com webhook e API REST
description: Tutorial para desenvolvedores: como integrar a WhatsApp Cloud API com webhook assinado (X-Hub-Signature-256), envio por API REST em Node.js e status de entrega.
date: 2026-09-06
category: integracao
tags: whatsapp cloud api, webhook, api rest, node.js, x-hub-signature-256, idempotência
author: gabriel-miranda
cover: /blog-assets/covers/whatsapp-cloud-api-como-integrar-webhook.jpg
coverAlt: Código de programação exibido em uma tela
coverSource: https://www.pexels.com/photo/14553707/
---

Integrar a WhatsApp Cloud API é montar três peças: um número autorizado na Meta com um token de acesso, um webhook que recebe mensagens e status de entrega, e chamadas REST para enviar. O webhook chega assinado com HMAC-SHA256 no header `X-Hub-Signature-256`, a Meta reenvia o que não recebeu `200` e, fora da janela de 24 horas, só sai template aprovado. O resto deste guia é o detalhe de cada peça, com código em Node.js, e a diferença entre fazer tudo direto na Meta ou por um Tech Provider.

O público aqui é quem escreve código. Se você quer entender o produto antes de entrar na técnica, comece pela visão geral da [API oficial do WhatsApp](/blog/api-oficial-do-whatsapp).

## As três peças de qualquer integração

Toda integração com a Cloud API, em qualquer linguagem e com qualquer provedor, se reduz a isto:

| Peça | O que é | De quem é a regra |
|---|---|---|
| Número autorizado + token | Um número registrado na WhatsApp Business Platform e um token com permissão para operá-lo | Meta |
| Webhook (entrada) | Um endpoint HTTPS seu que a Meta chama a cada mensagem recebida e a cada mudança de status | Meta assina e reenvia; você valida e deduplica |
| API REST (saída) | `POST` para enviar texto, mídia, botões ou template | Meta define formato, janela e limites |

A parte que mais engana é a primeira. Enviar e receber é código simples. Chegar a um número autorizado, com o app aprovado e o token certo, é onde a maioria dos tutoriais em português gasta 80% do texto e onde a maioria das integrações trava. Por isso vale separar os dois caminhos antes do código.

## Dois caminhos: direto na Meta ou por um Tech Provider

### O que você faz sozinho: app, App Review, verificação, token permanente e /register

Integrando direto, a sequência é sua. Você cria um app no Meta for Developers, adiciona o produto WhatsApp, verifica a empresa no Business Manager e pede App Review para as permissões `whatsapp_business_management` e `whatsapp_business_messaging`. Sem o review aprovado, o app só fala com números de teste. O token de desenvolvimento expira em horas; para produção você cria um usuário de sistema no Business Manager e gera um token permanente com essas permissões. Depois cadastra o webhook no App Dashboard, escolhe os campos assinados e registra o número com `POST /{phone_number_id}/register`.

Cada passo tem um jeito de falhar sem erro claro. O mais comum é esquecer o `/register`: o token funciona, o webhook responde, e todo envio devolve o código 133010, "número não registrado". O segundo é assinar só `messages` no App Dashboard e depois se perguntar por que aprovação de template e queda de qualidade nunca chegam.

### O que o provedor já resolveu: onboarding, token, webhooks, dedup, retry

Por um Tech Provider verificado pela Meta, o número entra pelo [Embedded Signup](/blog/embedded-signup-whatsapp): uma janela da Meta, aberta dentro do site do provedor, onde o dono do número autoriza o app do provedor a operar aquele número. A Meta devolve os ids e um `code` trocável por token. O App Review é do provedor, o token fica guardado cifrado no provedor, os webhooks da Meta chegam no provedor, e o que chega até você é um evento já deduplicado, com retry e assinatura própria.

Isso não muda o que a Meta cobra. Como Tech Provider, o Frame Conexa não fatura mensagem e não cadastra pagamento na Meta pelo cliente: o cartão entra no Business Manager do dono do número, e a Meta cobra dele direto pela tabela por mensagem. O provedor cobra pelo software. A diferença entre os dois caminhos é quem carrega o App Review, a rotação de token e a fila de reentrega, não quanto custa cada mensagem.

Há uma diferença técnica importante entre as modalidades de número. Em [coexistência](/blog/coexistencia-whatsapp-business-api), o número continua no app WhatsApp Business do celular e entra na API ao mesmo tempo; o app já registrou o número, e chamar `/register` nele derruba o app do celular. Num número novo, exclusivo da API, o `/register` é obrigatório. O provedor sabe qual é o caso e faz a chamada certa; integrando direto, você precisa saber.

## Receber mensagens: o webhook

O webhook é um endpoint HTTPS público, com certificado válido, que responde a dois verbos. `GET` é o handshake de cadastro. `POST` é o que a Meta chama a cada evento.

### Handshake de verificação: hub.mode, hub.verify_token, hub.challenge

Ao salvar a URL no App Dashboard, a Meta faz um `GET` com três parâmetros: `hub.mode=subscribe`, `hub.verify_token` com o valor que você mesmo digitou no painel dela, e `hub.challenge`, um número. Se o token bate, você devolve o `hub.challenge` como corpo, em texto, com `200`. Qualquer outra resposta e o cadastro falha.

```javascript
app.get('/webhook', (req, res) => {
  const mode = req.query['hub.mode'];
  const token = req.query['hub.verify_token'];
  const challenge = req.query['hub.challenge'];
  if (mode === 'subscribe' && token === process.env.VERIFY_TOKEN) {
    return res.status(200).send(challenge);
  }
  return res.sendStatus(403);
});
```

O `verify_token` é um segredo que você inventa. Ele só serve para o handshake; não assina evento nenhum. Compare em tempo constante se quiser ser rigoroso, mas o valor importante é o próximo.

### Validar a assinatura HMAC-SHA256 sobre o corpo bruto

Toda notificação vem com o header `X-Hub-Signature-256`, no formato `sha256=<hex>`. O hex é o HMAC-SHA256 do corpo da requisição, calculado com o App Secret do seu app. Recalcular e comparar é o que garante que o `POST` veio da Meta e não de qualquer um que descobriu a URL. A regra está na [documentação de webhooks da Graph API](https://developers.facebook.com/docs/graph-api/webhooks/getting-started).

O detalhe que quebra a maioria das implementações: o HMAC é sobre os **bytes exatos** do corpo. Se um parser JSON rodar antes e você reserializar o objeto, a ordem das chaves e os espaços mudam, e o hash nunca bate. Leia o corpo cru primeiro, valide, e só então faça o parse.

```javascript
import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const received = req.get('X-Hub-Signature-256') || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.APP_SECRET)
    .update(req.body)
    .digest('hex');

  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401);
  }

  res.sendStatus(200);
  enqueue(JSON.parse(req.body.toString('utf8')));
});
```

Dois pontos nesse código são deliberados. `timingSafeEqual` em vez de `===`, porque uma comparação que para no primeiro byte diferente vaza, pelo tempo de resposta, quantos bytes acertaram. E o `200` sai **antes** do processamento: a Meta mede o tempo de resposta e reenvia o que demora. Grave o evento numa fila ou numa tabela e responda; o processamento acontece depois.

### O envelope: messages, statuses, errors

O corpo tem sempre a mesma casca: `object`, uma lista `entry`, cada entrada com uma lista `changes`, e cada mudança com um `field` e um `value`. Para o campo `messages`, o `value` traz `messaging_product`, `metadata` com o número da empresa, e então `contacts` e `messages` quando alguém escreveu, ou `statuses` quando uma mensagem sua mudou de estado. A referência completa está na [visão geral dos webhooks do WhatsApp](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview).

Uma mensagem recebida:

```json
{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "WABA_ID",
    "changes": [{
      "field": "messages",
      "value": {
        "messaging_product": "whatsapp",
        "metadata": { "display_phone_number": "5585999990000", "phone_number_id": "PHONE_NUMBER_ID" },
        "contacts": [{ "profile": { "name": "Ana" }, "wa_id": "5585988880000" }],
        "messages": [{
          "from": "5585988880000",
          "id": "wamid.HBgNNTU4NT...",
          "timestamp": "1757340000",
          "type": "text",
          "text": { "body": "Oi, ainda tem vaga para sábado?" }
        }]
      }
    }]
  }]
}
```

Um status da sua mensagem:

```json
{
  "field": "messages",
  "value": {
    "messaging_product": "whatsapp",
    "metadata": { "phone_number_id": "PHONE_NUMBER_ID" },
    "statuses": [{
      "id": "wamid.HBgNNTU4NT...",
      "status": "delivered",
      "timestamp": "1757340042",
      "recipient_id": "5585988880000",
      "conversation": { "id": "CONVERSATION_ID", "origin": { "type": "service" } },
      "pricing": { "billable": false, "pricing_model": "PMP", "category": "service" }
    }]
  }
}
```

Os quatro valores de `status` contam uma história em ordem: `sent` significa que a Meta aceitou e encaminhou; `delivered`, que chegou ao aparelho; `read`, que a pessoa abriu, quando ela não desligou a confirmação de leitura; `failed`, que não foi, e aí `statuses[].errors` traz o código. É por esse `failed` que você fica sabendo de um 131047 ou de um 131026 depois que a API já tinha respondido `200` ao seu `POST`. O envio aceito não é o envio entregue.

O objeto `pricing` é o que diz se aquela mensagem custa. `billable` responde se a Meta cobra, `category` diz a categoria e `pricing_model: "PMP"` indica o modelo por mensagem, que substituiu o antigo modelo por conversa. Vale guardar: é a única fonte de verdade sobre custo que chega até o seu sistema.

> **Atenção:** mensagens de serviço, as respostas livres dentro da janela de 24 horas, são gratuitas até 30/09/2026. A partir de 01/10/2026 a Meta passa a cobrá-las por mensagem, e o mesmo vale para templates de utilidade enviados dentro da janela. O `pricing.billable` desses status vai mudar de `false` para `true` nessa data. Detalhes na [página oficial sobre mensagens não-template](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages).

Erros aparecem em três lugares: `value.errors` para problemas do evento inteiro, `messages[].type: "unsupported"` para conteúdo que a API não representa, e `statuses[].errors` para falha de entrega. Um receptor que só lê `messages[].text.body` ignora os três.

### Reentregas e duplicatas: por que deduplicar é obrigatório

Quando o seu endpoint não responde `200`, a Meta reenvia. A documentação do WhatsApp fala em novas tentativas com frequência decrescente por até sete dias; a documentação genérica de webhooks fala em 36 horas. Nos dois casos, a consequência é a mesma: o mesmo evento pode chegar mais de uma vez, e também pode chegar fora de ordem, um `delivered` antes do `sent`.

A chave de deduplicação natural é `messages[].id`, o `wamid`, e para status o par `statuses[].id` mais `status`. Um `INSERT` com restrição única e `ON CONFLICT DO NOTHING` resolve; processar o evento e só depois responder `200` não resolve, porque o timeout da Meta é curto e um processamento lento vira reentrega de algo que você já tratou. Se o seu banco falhar no `INSERT`, responda `500` e deixe a Meta reenviar. A fila de retry externa é ela.

## Enviar mensagens: API REST

O envio é um `POST` para `https://graph.facebook.com/v25.0/{phone_number_id}/messages`, com `Authorization: Bearer` e um corpo JSON que começa com `messaging_product: "whatsapp"`. O número de destino vai em E.164 sem o sinal de mais.

### Texto dentro da janela e template fora dela

O que pode sair depende de uma regra que é da Meta, não do provedor: mensagem livre só dentro da [janela de 24 horas](/blog/janela-de-24-horas-whatsapp) aberta pela última mensagem da pessoa. Fora dela, só template aprovado, nas categorias marketing, utilidade ou autenticação.

Texto, quando a janela está aberta:

```bash
curl -X POST "https://graph.facebook.com/v25.0/$PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer $META_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "5585988880000",
    "type": "text",
    "text": { "body": "Tem sim. Sábado às 10h ou às 14h?" }
  }'
```

Template, quando a janela está fechada, em Node.js com `fetch` nativo:

```javascript
const res = await fetch(`https://graph.facebook.com/v25.0/${PHONE_NUMBER_ID}/messages`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.META_ACCESS_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    messaging_product: 'whatsapp',
    to: '5585988880000',
    type: 'template',
    template: {
      name: 'lembrete_agendamento',
      language: { code: 'pt_BR' },
      components: [
        { type: 'body', parameters: [{ type: 'text', text: 'sábado, 10h' }] },
      ],
    },
  }),
});
const data = await res.json();
if (!res.ok) throw Object.assign(new Error(data.error?.message), { graph: data.error });
const wamid = data.messages[0].id;
```

O `wamid` que volta é o mesmo `id` que vai aparecer nos status do webhook. Guarde-o na hora: é ele que liga o envio ao `delivered` e ao `failed` que chegam minutos depois. Como criar um template e o que a Meta olha na aprovação está em [template do WhatsApp aprovado](/blog/template-whatsapp-aprovado).

### Idempotência, retry e os códigos 130429, 131047, 133010 e 190

A API responde erro em dois momentos: síncrono, no `POST`, e assíncrono, num status `failed`. Os que importam para o código de envio:

| Código | O que significa | O que fazer | De quem é |
|---|---|---|---|
| 130429 | Ritmo de envio acima do que o número suporta | Espere e reenvie com espaçamento; backoff exponencial curto | Meta |
| 131047 | Mais de 24 horas desde a última mensagem da pessoa | Não reenvie o texto; envie um template aprovado | Meta |
| 131026 | Destinatário não é usuário do WhatsApp, não aceitou os termos ou usa app antigo | Tire da lista; reenviar dá o mesmo resultado | Meta |
| 133010 | Número não registrado na plataforma | Chame `/register` no número novo; nunca em coexistência | Meta |
| 190 | Token expirado ou revogado | Gere token novo, ou refaça a autorização; nada mais vai sair até lá | Meta |

A lista completa, com o que o painel mostra para cada um, está em [erros da API do WhatsApp](/blog/erros-whatsapp-api-131047-131026-131049) e na [referência oficial de códigos](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes).

Sobre ritmo: um número integrado direto começa em 80 mensagens por segundo, segundo a documentação de throughput da Meta; um número em coexistência fica em 20. Acima disso vem o 130429, e a resposta certa é espaçar, não insistir em loop.

Sobre retry: a Cloud API não tem chave de idempotência nativa. Se o seu `POST` estourar o timeout sem resposta, você não sabe se a mensagem saiu. Reenviar às cegas duplica; não reenviar pode perder. A saída é registrar a intenção de envio **antes** do `POST`, com um id seu, e só marcar como enviada quando o `wamid` voltar. Um envio sem `wamid` fica como incerto até um status chegar pelo webhook, ou até você conferir por outro caminho. É trabalhoso, e é exatamente o tipo de coisa que um provedor faz por você.

## Como fica com o Frame Conexa

Com o número conectado pelo Embedded Signup, as três peças ficam assim: o token é um `fc_` gerado em Configurações, no painel, e enviado em `Authorization: Bearer`; o webhook é uma URL sua cadastrada por API ou pelo painel; o envio é o mesmo formato da Cloud API, numa rota que já conhece a instância. A [documentação](/docs) tem o quickstart e a referência de cada rota; abaixo vai o que muda em relação à integração direta.

### Webhook assinado com instanceId, customerRef e payload cru da Meta

Você cadastra a URL com `PUT /api/v1/webhook-config` e o corpo `{ "url": "https://..." }`. A resposta traz o `webhookSecret`, gerado na primeira gravação; `POST /api/v1/webhook-config/secret` gira o segredo quando você precisar, e o anterior morre na hora.

O que chega no seu endpoint é um envelope assinado no header `X-Frame-Signature-256`, no mesmo esquema da Meta: `sha256=` mais o HMAC-SHA256 do corpo bruto, calculado com esse segredo. O código de validação é o mesmo da seção anterior, trocando o nome do header e o segredo:

```javascript
app.post('/webhooks/frame-conexa', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('X-Frame-Signature-256') || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.FRAME_WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');
  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
  res.sendStatus(200);
  enqueue(JSON.parse(req.body.toString('utf8')));
});
```

O envelope tem seis chaves de topo, e `payload` é o `value` cru da Meta, sem nenhum campo nosso dentro:

```json
{
  "type": "messages",
  "clientId": "8",
  "instanceId": "6ed76b84-4d65-4f3d-88c1-8d4174bdce73",
  "customerId": null,
  "customerRef": null,
  "payload": { "messaging_product": "whatsapp", "metadata": {}, "messages": [] }
}
```

Três tipos de evento chegam por aí. `messages` é o campo de mesmo nome da Meta, com mensagens recebidas e status de entrega juntos, como ela manda. `smb_message_echoes` só existe em coexistência: é a mensagem que alguém enviou pelo app do celular, para o seu sistema saber que a conversa foi respondida por ali. `instance_status` é nosso: `connected` ou `disconnected`, emitido uma vez por transição, para o seu sistema descobrir que o número caiu antes do primeiro envio que falha com 409.

Duas decisões desse envelope são de compatibilidade. `instanceId` é o UUID que aparece como `id` em `GET /api/v1/instances`, e é por ele que você casa o evento com uma instância; `clientId` é uma chave numérica antiga que segue existindo porque trocá-la quebrou consumidores em produção. E `customerId`/`customerRef` vêm como `null`, nunca ausentes: são o cliente final numa parceria de software, e um consumidor estrito trata chave nula e chave ausente como coisas diferentes.

A entrega é durável do nosso lado. O evento entra numa fila antes de sair, o `POST` tem timeout de 10 segundos, e uma resposta que não seja 2xx agenda nova tentativa com espaçamento crescente, até esgotar; só então o evento vai para uma fila de descarte, visível no painel. Um destino lento ou fora do ar não perde evento nem trava a ingestão dos outros. Do seu lado, continue deduplicando por `messages[].id`: a Meta pode reentregar para nós, e nós reentregamos para você.

### POST /api/v1/instances/:id/messages com token fc_ e Idempotency-Key

O envio é o mesmo corpo da Cloud API, sem `messaging_product` e sem `phone_number_id`, porque a instância na URL já diz qual número é:

```bash
curl -X POST "https://frameconexa.com/api/v1/instances/$FRAME_CONEXA_INSTANCE_ID/messages" \
  -H "Authorization: Bearer fc_SEU_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f8d4a11-5e52-4c83-a72c-45d48f4ea6d5" \
  -d '{"to":"5585988880000","type":"text","text":{"body":"Tem sim. Sábado às 10h ou às 14h?"}}'
```

O `Idempotency-Key` é o que resolve o problema do retry descrito acima. Use um UUID por mensagem. Se a resposta se perder, repita a mesma chave com o mesmo corpo: você recebe o resultado anterior em vez de um envio novo. Chave repetida com corpo diferente responde `409 IDEMPOTENCY_CONFLICT`; envio ainda em processamento responde `409 SEND_IN_PROGRESS` com `ambiguous: true` e não é repetido.

Por dentro, o envio é DB-primeiro: a intenção vira linha antes do `POST` à Meta. Aceite vira `accepted`, erro explícito vira `failed`, timeout sem `wamid` vira `unknown`, e nesse último caso a resposta traz `ambiguous: true`. Significa "pode ter saído": não reenvie sem conferir. Quem promove `accepted` para entregue ou lido é o status do webhook, nunca a resposta do `POST`.

Template segue o formato da Cloud API: `"type": "template"` com `name`, `language` e `components`. Duas regras do lado do provedor valem aqui: template de marketing e a criação de templates exigem número do tipo marketing, e a categoria do template é conferida antes do envio. Um número de atendimento envia utilidade e autenticação sem restrição. Os limites de chamada estão em [erros e limites](/docs/errors-and-limits): 120 requisições por minuto por token, com `Retry-After` no 429.

### Baixar mídia recebida pelo proxy

Quando alguém manda uma foto ou um documento, o webhook entrega só `image.id` ou `document.id`. O binário fica na Meta e exige o token do número para ser baixado, em dois passos: um `GET` para o metadado, que devolve uma URL de CDN válida por minutos, e um segundo `GET` nessa URL, também com o token. Integrando direto, você faz os dois. Pelo Conexa, um `GET` resolve:

```bash
curl -L "https://frameconexa.com/api/v1/instances/$FRAME_CONEXA_INSTANCE_ID/media/$MEDIA_ID" \
  -H "Authorization: Bearer fc_SEU_TOKEN" \
  -o recebido.jpg
```

O proxy faz stream, com teto de 100 MB, e nunca devolve nem cacheia a URL de CDN. Baixe assim que o evento chegar: a retenção do lado da Meta é limitada.

Se o seu caso é um fluxo de automação em vez de código próprio, as mesmas rotas alimentam o nó HTTP do [n8n e do Make](/blog/whatsapp-business-api-n8n-make). Se é um agente de IA que opera o número por linguagem natural, há um [servidor MCP](/blog/mcp-whatsapp-agente-de-ia) sobre a mesma API, com confirmação dupla antes de qualquer envio.

## Checklist de produção

Antes de apontar tráfego real para a integração, confira:

- **Assinatura validada sobre o corpo bruto**, com comparação em tempo constante, e `401` para o que não bate. Um webhook sem validação aceita qualquer `POST` como se fosse da Meta.
- **`200` rápido e processamento em fila.** Guarde primeiro, processe depois. Se não conseguir guardar, responda `500` e deixe reenviar.
- **Deduplicação por `wamid`** para mensagens e por `wamid` mais `status` para status. Eventos repetidos e fora de ordem são normais.
- **`wamid` guardado no envio**, ligando o `POST` aos status que chegam depois. Sem isso, `failed` não tem a quem pertencer.
- **Idempotência no envio**: chave por mensagem, e nunca reenviar um resultado incerto às cegas.
- **Janela de 24 horas no código**, não só na cabeça: o texto livre só sai quando a última mensagem da pessoa tem menos de 24 horas; do contrário, template. Contas com anúncio Click-to-WhatsApp têm uma janela grátis de 72 horas que o painel sinaliza, e o artigo sobre [anúncios Click-to-WhatsApp](/blog/click-to-whatsapp-anuncio) explica a condição para ela existir.
- **Tratamento do 190**: token revogado significa que nada mais sai até reautorizar. Alerta para o time, não retry.
- **Versão da Graph API fixada** na URL. Chamada a versão expirada cai para a mais antiga disponível, silenciosamente.
- **Custo previsto para outubro de 2026**: se a sua integração responde muito dentro da janela, o `pricing.billable` dessas mensagens vai virar `true` em 01/10/2026. A tabela por mensagem é a publicada pela Meta; confira o rate card oficial. O que um provedor cobra, e o que a Meta cobra, está separado em [quanto custa a WhatsApp Business API](/blog/whatsapp-business-api-preco).

## Perguntas frequentes

### Preciso criar um app na Meta para integrar a Cloud API?

Integrando direto, sim: app no Meta for Developers, produto WhatsApp, verificação da empresa e App Review para as duas permissões do WhatsApp. Por um Tech Provider verificado, o app é do provedor e o número entra pelo Embedded Signup; você recebe um token do provedor e nunca toca no App Dashboard da Meta. O que não muda em nenhum dos caminhos é o pagamento: o cartão entra no Business Manager do dono do número, e a Meta cobra dele direto.

### Como validar a assinatura do webhook do WhatsApp?

Leia o corpo da requisição como bytes, antes de qualquer parser JSON. Calcule o HMAC-SHA256 desses bytes com o App Secret do app, prefixe com `sha256=` e compare com o header `X-Hub-Signature-256` em tempo constante. Se bater, responda `200` e processe; se não, `401`. No webhook do Frame Conexa o header é `X-Frame-Signature-256` e o segredo é o `webhookSecret` da sua conta; o código é o mesmo.

### Por que recebo o mesmo evento duas vezes?

Porque a Meta reenvia tudo que não recebeu `200` a tempo, com tentativas em frequência decrescente por até sete dias segundo a documentação do WhatsApp. Um processamento lento antes da resposta, um deploy no meio, um timeout de rede: qualquer um gera reentrega. Deduplique por `messages[].id` e, para status, pelo par id mais `status`. Se você recebe pelo Conexa, vale a mesma regra: nós reentregamos o que o seu endpoint não confirmou.

### Como enviar mensagem fora da janela de 24h pela API?

Só com template aprovado pela Meta, enviado com `"type": "template"`, o nome, o idioma e os parâmetros dos componentes. Texto livre fora da janela responde 131047 e não sai. A categoria do template define o que pode ser dito e quanto custa: marketing para promoção, utilidade para pedido e agendamento, autenticação para código de uso único.

### Que versão da Graph API devo usar em 2026?

A mais recente estável que você tiver testado. Em setembro de 2026, a v25.0 foi lançada em 18/02/2026 e vale até 29/07/2028; a v26.0 saiu em 29/07/2026. Cada versão vive cerca de dois anos depois da seguinte. A v20.0 é removida em 24/09/2026, e chamadas a uma versão expirada caem para a mais antiga disponível. A tabela oficial está na [lista de versões da Graph API](https://developers.facebook.com/docs/graph-api/changelog/versions/). Pelo Conexa a versão é do provedor, e você não a escreve na URL.

## Próximo passo

O [quickstart](/docs/quickstart) leva do token à primeira mensagem em poucos minutos, e o [guia de webhooks](/docs/guides/webhooks) tem o envelope completo com o schema de cada tipo de evento. Para conectar um número e gerar o primeiro token, veja os [planos por número](/#precos).
