Implementação

Criar um atendimento de WhatsApp do zero com a API oficial

Caixa de entrada compartilhada por uma equipe Ilustração de uma fila única de conversas em que cada item mostra quem da equipe assumiu o atendimento. Uma fila para a equipe quem assumiu fica visível

Resposta curta: uma tela de conversa parecida com o WhatsApp Web, funcionando com a API oficial em coexistência, sai em três fases: interface, banco de dados e conexão com a API. É uma tela só, com a lista de conversas à esquerda e as mensagens à direita. O projeto de referência, construído por Israel Henrique, CTO da Datafy, usa Claude Design para o layout, VS Code com Claude Code ou Codex para escrever, Nuxt no front, Supabase com duas tabelas, Pusher para tempo real, a Datafy API para o WhatsApp e a Vercel para publicar, com o código liberado no GitHub. A ordem que faz diferença: coletar payloads reais antes de criar qualquer tabela.

3 fasesinterface, banco e conexão com a API
2 tabelasconversas e mensagens
20 e 50conversas e mensagens por página
100conexões simultâneas no plano grátis do Pusher

Principais pontos

Como o webhook chega no seu servidor Cliente escreve no WhatsApp Datafy API POST com o payload da Meta Seu servidor responde 200 em até 20 s Idempotência Guarde o x-datafy-delivery-id e ignore a entrega que já viu. Como validar x-datafy-signature-256, HMAC de {timestamp}.{corpo cru}

O que o projeto é, e o que não é

É uma tela única que recebe e envia mensagens pelo número conectado, em coexistência: a mesma linha continua funcionando no aplicativo do celular, e mensagem enviada do celular aparece na aplicação, com legenda quando é imagem.

Ele delimita desde o começo: não é um SaaS, não tem login, assinatura nem multiusuário. "O foco é entender o funcionamento da API, enviar e receber mensagens." Quem precisa de caixa de entrada para uma equipe liga a Datafy a um sistema de atendimento pronto.

As ferramentas

Camada Ferramenta Papel
Design Claude Design Gera a interface a partir de um print do WhatsApp Web
Código VS Code com Claude Code ou Codex Escreve os componentes e o servidor
Front Nuxt com Tailwind Tela única, componentizada, modo claro e escuro
Banco Supabase Duas tabelas, e as políticas de acesso
Tempo real Pusher Entrega a mensagem na tela sem recarregar
WhatsApp Datafy API API oficial, com webhook e token no painel
Publicação Vercel Importa o repositório e sobe com as variáveis de ambiente

A razão de usar uma parceira de tecnologia da Meta é a lista de etapas que desaparece: conta de desenvolvedor, aplicativo, infraestrutura de webhook, revisão, documentos e ainda se tornar Tech Provider para ter coexistência. Por que a API é um espelho da Cloud API.

Fase 1: a interface, com dados falsos

A tela tem duas áreas, e a divisão vira a árvore de componentes: à esquerda, conversas com cabeçalho, lista e item; à direita, mensagens com cabeçalho, lista, item e o campo de digitação flutuante. Os balões são por tipo, texto, áudio, imagem com ou sem legenda, documento, vídeo e figurinha, e os de mídia recebem um link público para renderizar. Um componente auxiliar compartilhado desenha só o horário e os tiques de status em todo balão. Modo claro e escuro entram nessa fase, porque acertar contraste depois custa mais.

Uma prática dessa fase vale para qualquer projeto com IA: criar um arquivo de documentação com o roteiro, com fases, estrutura de pastas e o que já foi feito. Ele mesmo diz que deveria ter feito isso no começo, e a utilidade apareceu no meio: quando a ferramenta de IA travou, a outra leu o mesmo arquivo e continuou de onde havia parado.

Fase 2: os payloads vêm antes das tabelas

É o que separa um banco que funciona de um que precisa de três migrations. Antes de criar tabela, mande para o número mensagens de tipos diferentes e copie o payload de cada uma no log ao vivo do painel. São cinco:

Envio de teste O que o payload ensina
Texto pelo celular do cliente A estrutura básica, com o contato e o corpo da mensagem
Áudio Que a mídia vem com um identificador, não com o arquivo
Imagem com legenda Que a legenda existe e precisa de coluna
Mensagem enviada pelo app do WhatsApp Business Que ela chega em outro evento, o eco do celular
Envio pela própria API Que o retorno é só status, sem conteúdo

O primeiro comando é o que devolve os seus identificadores, porque quem tem só o token não sabe o phone_number_id:

GET https://cloud.datafyapi.com.br/me
Authorization: Bearer sk_live_xxx
{
  "cliente_id": "uuid-do-cliente",
  "phone_number_id": "106540352242922",
  "waba_id": "366634483210360",
  "business_id": "123456789"
}

E o miolo do payload de uma mensagem recebida, que é o molde das duas tabelas:

{
  "metadata": { "phone_number_id": "106540352242922" },
  "contacts": [{
    "profile": { "name": "Israel Henrique" },
    "wa_id": "5541999999999",
    "user_id": "ABCDEF1234567890"
  }],
  "messages": [{
    "from": "5541999999999",
    "id": "wamid.HBgNNTU0MTk5OTk5OTk5ORUCABIYFjNBM...",
    "timestamp": "1759100000",
    "type": "image",
    "image": { "mime_type": "image/jpeg", "id": "1402849023948572", "caption": "nota fiscal" }
  }]
}

Como abrir esse log e copiar payload por payload.

Três coisas que os payloads ensinam

O phone_number_id é do número conectado, não de quem mandou. Vem igual em toda mensagem. Quem trata esse campo como identificação do cliente erra a modelagem inteira, e é justamente o campo a guardar se um dia quiser mais de um número.

Quem mandou está em contacts, com nome, telefone e user_id, o identificador novo do WhatsApp nas APIs. A expectativa dele é que o telefone deixe de vir em algum momento, com a ressalva de que é previsão e não data. Guarde o user_id desde a primeira versão do banco. Sobre o user_id.

Envio pela API volta só como status, com o ID e sent, depois delivered, e read quando a pessoa tem a confirmação de leitura ativada. Se não tiver, o evento de leitura não chega.

As duas tabelas, e a primeira versão errada

Duas tabelas bastam. Conversas guarda o contato, o phone_number_id, o identificador da conta, o nome, o user_id e o texto da última mensagem. Mensagens guarda a conversa, o ID, a direção, o tipo, o corpo, a mídia e o horário. A primeira versão gerada pela IA tinha colunas que a API oficial não manda, e faltava uma que ela manda:

Coluna proposta Decisão
URL de avatar Removida: a API oficial não envia foto de perfil
Contador de não lidas Removida: não vem no payload
Tipo do arquivo e marcação de áudio de voz Removidas nesse escopo
Legenda da mídia Acrescentada: imagem e vídeo chegam com legenda

Como a primeira migration já havia sido aplicada, a correção veio numa segunda: migration aplicada não se reescreve.

Não dê à IA o controle do banco

A IA sugeriu aplicar as mudanças pela linha de comando, controlando o banco direto. A recusa é explícita: "o banco de dados é o coração do projeto. Se dá o controle para ele, ele pode fazer muita coisa errada. No máximo você dá permissão para ela ler o teu banco de dados, mas nunca para mexer."

O combinado: a IA diz o que precisa e gera o SQL, e você cola no editor do banco e executa.

Segurança: chaves e políticas

As duas chaves do banco são diferentes. A pública pode aparecer no front. A de serviço nunca: fica só no servidor. Ele regenerou a de serviço depois de ela aparecer na gravação, que é o procedimento certo quando uma credencial é exposta.

O arquivo de exemplo de variáveis vai para o repositório público, e foi a IA que avisou que havia uma credencial real colada ali. Nele entram só os nomes das variáveis.

As políticas públicas saíram. A IA havia criado políticas que deixavam as tabelas legíveis por qualquer um com a chave pública. A correção foi removê-las e mover todas as consultas para uma pasta de servidor.

O token do número é rotacionável no painel. As variáveis do projeto são três: URL base da Datafy, token e phone_number_id.

Assinatura do webhook. Os webhooks da Datafy entregam os eventos com assinatura HMAC, nos cabeçalhos x-datafy-signature-256 e x-datafy-timestamp, mais o x-datafy-delivery-id, que serve de chave de idempotência. Ao validar, use o corpo cru da requisição: parsear e reserializar muda os bytes e a assinatura não fecha.

Desempenho: paginação, rodapé e cache

Conversas de 20 em 20, mensagens de 50 em 50, carregando mais conforme a rolagem. A lista abre na mensagem mais recente e rola para o fim também ao enviar, e mesmo com uma única mensagem ela fica colada embaixo. Detalhe da prática: mídia carrega de forma assíncrona e empurra a altura depois da rolagem, então a tela pode parar alguns pixels acima do fim.

Sem cache, voltar para uma conversa já aberta refaz a mesma consulta de 50 mensagens. Um armazenamento local por conversa resolve, e é o mesmo ponto de inserção que o tempo real usa para acrescentar a mensagem nova e mover a conversa para o topo.

Fase 3: webhook primeiro, tempo real depois

A ordem recomendada pela própria IA está certa: o webhook é a fundação e pode ser testado sozinho, sem conta externa.

  1. Crie o endpoint que recebe o evento, identifica ou cria a conversa e grava a mensagem.
  2. Suba um túnel para a porta da aplicação.
  3. Cadastre a URL no painel da Datafy, na aba de webhooks do número, marcando os dois eventos que importam: mensagens recebidas e mensagens enviadas pelo celular.
  4. Use o botão de teste do painel, que dispara um evento falso antes de existir tráfego real.

O primeiro teste dele deu 403, e a causa não era a Datafy: era o próprio framework, que bloqueia hosts não autorizados em desenvolvimento. Liberado o domínio do túnel e reiniciado o servidor, o evento voltou 200. A interface local do túnel, na porta 4040, mostra o que chegou mesmo quando a aplicação recusou.

Mídia: o identificador vira URL

O payload de mídia não traz arquivo, traz um identificador, e a URL que aparece ali não abre no navegador. O caminho é uma chamada com o ID:

GET https://cloud.datafyapi.com.br/media/1402849023948572
Authorization: Bearer sk_live_xxx
{
  "url": "https://files.datafyapi.com.br/media/1402849023948572.jpg",
  "mime_type": "image/jpeg",
  "file_size": 184320
}

Essa URL é pública e vale 30 dias. O endpoint simplificado existe porque a mídia chega criptografada da Meta, e a descriptografia já foi resolvida na plataforma. No projeto, isso vira uma função chamada quando o webhook traz mídia, e o que vai para o banco é o link resolvido. Dois prazos não se confundem: o identificador do webhook expira em 7 dias na Meta, e a URL de download da Meta vale poucos minutos. O caminho completo da mídia.

Envio pelo campo de digitação

O campo de digitação chama um endpoint do seu servidor, que chama a Datafy:

POST https://cloud.datafyapi.com.br/v1/{phone_number_id}/messages
Authorization: Bearer sk_live_xxx
Content-Type: application/json

{
  "messaging_product": "whatsapp",
  "to": "5541999999999",
  "type": "text",
  "text": { "body": "Oi! Estou verificando o seu pedido agora." }
}

A resposta traz um ID e nada mais: o pedido foi aceito, não entregue. Na tela, o balão aparece na hora, de forma otimista, e o status e o eco chegam depois pelo webhook, sem duplicar, porque o ID é o mesmo. Para marcar como lida a mensagem do cliente há o endpoint simplificado POST /messages/read, que não pede o phone_number_id. Como a URL é a da Datafy e o corpo é o da documentação da Meta, qualquer modelo de IA já sabe montar essas chamadas.

Tempo real, e o que a tela precisa tratar

O caminho é de mão única: o webhook chega no servidor, que grava no banco e publica no serviço de tempo real, e a tela recebe. O front nunca fala direto com o banco, e o plano gratuito do Pusher atende até 100 conexões simultâneas.

Falta a parte que nenhum layout resolve, que são as regras da API oficial:

Situação O que a tela precisa fazer
Janela de 24 horas aberta Campo de digitação livre, mensagem de serviço
Janela fechada Bloquear o envio livre e exigir template
Requisição aceita com ID Mostrar como enviando, nunca como entregue
sent Um tique
delivered Dois tiques
read Dois tiques azuis, quando a confirmação existe
failed Marcar erro na bolha, por exemplo serviço fora da janela ou falha no pagamento

Quem abre e renova a janela é o cliente: resposta de atendente ou de automação não estende o prazo, e cada cliente tem a sua janela. Por isso a tabela de conversas guarda o horário da última mensagem recebida daquele contato, e a tela consulta esse horário antes de liberar o campo. Sem isso, a mensagem sai, volta HTTP 200 com ID, e a falha aparece depois no webhook de status, com o atendente achando que respondeu.

Publicar, e os limites do projeto

Código no repositório, projeto importado na plataforma de publicação e variáveis de ambiente coladas de uma vez. Depois do deploy vem o passo sem o qual nada funciona: trocar a URL do webhook no painel da Datafy, do túnel para a de produção. Enquanto o túnel está de pé, as mensagens continuam chegando por ele, o que dá a falsa impressão de que a publicação funcionou. Cuidado com a barra no fim da URL.

Dois limites ficam. É um número, por variável de ambiente: vários números mudam a modelagem, porque cada conversa passa a pertencer a um número. E a conversa apaga as mensagens em cascata, o que em mil mensagens apaga mil linhas de uma vez e pode travar.

Perguntas frequentes

Quantas tabelas o projeto precisa?

Duas: conversas e mensagens. As colunas certas saem dos payloads reais, não de suposição.

Por que coletar payloads antes de criar as tabelas?

Porque a API oficial manda menos do que a intuição espera, como a ausência de foto de perfil, e manda o que se esquece, como a legenda da mídia.

Posso deixar a IA aplicar as migrations?

A recomendação é não. A IA gera o SQL e você executa, porque no banco um erro dela não tem desfazer fácil.

Por que as consultas ao banco saem do servidor?

Porque as tabelas ficam sem acesso público, e só a chave de serviço lê. Ela nunca vai para o navegador.

O que fazer quando o webhook responde 403 no teste?

Olhe os hosts permitidos do seu framework em desenvolvimento, e a interface local do túnel na porta 4040 para ver o que chegou.

Como a tela sabe se pode enviar mensagem livre?

Guardando o horário da última mensagem recebida de cada cliente e comparando com as 24 horas. Fora da janela, só template, e a requisição pode ser aceita mesmo assim, falhando depois.

Isso serve como caixa de entrada para uma equipe?

Não como está: é um número, sem login e sem filas. Para atender com equipe, ligue a Datafy a um sistema de atendimento pronto.

Resumo para a sua integração

Siga a ordem: interface com dados falsos, coleta dos cinco payloads, duas tabelas modeladas em cima deles, webhook sem tempo real, mídia pelo identificador, envio pelo campo, tempo real e só então publicação. Guarde desde a primeira versão do banco o user_id, o phone_number_id, o ID da mensagem e o horário da última mensagem recebida: são os quatro campos que fazem diferença quando o projeto cresce.

Colete os cinco payloads antes de criar a primeira tabela

Conecte um número, chame GET /me para pegar o phone_number_id e mande para o número um texto, um áudio, uma imagem com legenda, uma mensagem pelo celular e um envio pela API. Copie os cinco payloads no log do painel e só então modele o banco.

Começar 7 dias grátis, sem cartão

Sem taxa de setup · Cancele quando quiser · Suporte por WhatsApp
Miniatura do vídeo: Como criar um WhatsApp Web do zero com a API oficial (tutorial completo) Como criar um WhatsApp Web do zero com a API oficial (tutorial completo)Canal DATA7 · 2 h 22 min · assistir no YouTube
O projeto inteiro na tela: componentes da interface, payloads reais virando tabelas, webhook por túnel, mídia, envio, tempo real e a troca da URL na publicação.

Leia também

Continue por aqui