WhatsApp, com você no outro celular.

A Cloud API da Meta Graph API v23.0 no localhost, roteada e validada contra o próprio arquivo OpenAPI da Meta. E você faz o papel do usuário: responde, lê, reage, manda um áudio, liga de volta para a empresa, ou faz a Meta reprovar um template, na hora que quiser. Todo webhook chega na sua máquina, assinado.

MOCK WHATSAPP
reprodução de uma execução
graph.facebook.com/v23.0 localhost:3010/v23.0
  1. 27/10/2026
  2. POST /100000000000001/message_templates 200
    appointment_reminder pt_BR UTILITY
    PENDING
  3. webhook message_template_status_update APPROVED
  4. POST /1234567890/messages 200
    template appointment_reminder
    "Olá Maria, sua consulta é amanhã às 14:30."
    accepted
  5. webhook status sent
  6. webhook status delivered
  7. webhook status read
  8. POST /1234567890/messages 200
    X-Mock-From: 5511987654321
  9. webhook messages de Maria Souza
    "Oi! Quero remarcar."
  10. POST /1234567890/messages 400
    template hello_world en_US
  11. (#132001) Template "hello_world" with language "en_US" not found

Uma execução real da imagem: as chamadas, os campos do webhook e o código de erro da Meta para um template que não existe. Só o papel é desenhado.

Rode na sua máquina.

Quatro jeitos, do mais rápido ao mais completo. A imagem é pública: nenhum login.

Imagem pronta

O jeito mais rápido: a imagem direto do GHCR.

docker pull ghcr.io/instituto-ama/whatsapp-mock:latest
docker run -d -p 3010:3010 ghcr.io/instituto-ama/whatsapp-mock:latest

Painel e API em localhost:3010

No seu docker-compose

Para subir junto com a sua aplicação, no compose dela.

services:
  whatsapp:
    image: ghcr.io/instituto-ama/whatsapp-mock:0.5.0
    ports:
      - '3010:3010'
    environment:
      WEBHOOK_URL: http://minha-app:4000/webhooks/whatsapp   # a sua aplicação
      WEBHOOK_SECRET: segredo-da-app                         # assina o x-hub-signature-256
      MOCK_ALLOWED_HOSTS: whatsapp                           # a sua aplicação chama por este nome
    volumes:
      - whatsapp-data:/app/data

volumes:
  whatsapp-data:

Da sua aplicação: http://whatsapp:3010/v23.0, com qualquer token bearer.

Gerar do código

Para rodar a partir do código-fonte, ou depois de alterar o mock.

git clone https://github.com/Instituto-AMA/ama4devs.git
cd ama4devs
docker build -t whatsapp-mock mocks/whatsapp
docker run -d -p 3010:3010 whatsapp-mock

Painel e API em localhost:3010

Subir tudo junto

Junto com o mock do Asaas e o Mailpit, já conectados: o Asaas manda os templates de aviso ao pagador por este mock.

git clone https://github.com/Instituto-AMA/ama4devs.git
cd ama4devs
make setup

WhatsApp em localhost:3010, Asaas em localhost:45445, e-mails em localhost:8025

A Meta, sem a fatura.

Testar contra a Meta exige um número real, uma empresa verificada, templates aprovados, uma URL pública para o webhook e um custo por conversa. E mesmo assim você não consegue fazer o usuário responder, atender uma ligação ou ter um template reprovado quando o teste precisa.

Fiel por construção

  • O arquivo OpenAPI da Meta para a v23.0 vem junto; toda requisição é roteada e validada contra ele.
  • Um teste de contrato passa todos os exemplos de requisição da especificação da Meta pelo mock.
  • Onde a especificação contradiz os próprios exemplos da Meta, a correção fica registrada com o motivo.
  • Os erros da Meta, com os códigos da Meta: 132001, 133010, 136025, 138018…

Estado que se comporta

  • Templates passam por revisão: aprovado, reprovado, pausado, com o motivo e o webhook de status.
  • Mensagens vão de enviada para entregue e lida.
  • Um número precisa ser verificado e registrado antes de enviar.
  • Uma ligação precisa da permissão do usuário. Nada para configurar: sobe com uma empresa, uma WABA e um número registrado com ligações ativadas.

Casos de uso.

O que a sua integração com o WhatsApp precisa aguentar, cada caso a poucas chamadas de distância. Qualquer token Authorization: Bearer é aceito, desde que exista.

Uma conversa inteira, dos dois lados

Prova que a sua caixa de entrada encadeia o template da empresa, as confirmações e a resposta do usuário do jeito que a Meta entrega. O header X-Mock-From faz a mesma chamada sair do celular do usuário.

curl -X POST localhost:3010/v23.0/1234567890/messages \
  -H 'Authorization: Bearer test' -H 'Content-Type: application/json' \
  -d '{"messaging_product":"whatsapp","to":"5511987654321","type":"template",
       "template":{"name":"appointment_reminder","language":{"code":"pt_BR"},
       "components":[{"type":"body","parameters":[
         {"type":"text","text":"Maria"},{"type":"text","text":"14:30"}]}]}}'
# → webhooks: sent, delivered, read

curl -X POST localhost:3010/v23.0/1234567890/messages \
  -H 'Authorization: Bearer test' -H 'Content-Type: application/json' \
  -H 'X-Mock-From: 5511987654321' -H 'X-Mock-Contact-Name: Maria Souza' \
  -d '{"messaging_product":"whatsapp","type":"text","text":{"body":"Oi! Quero remarcar."}}'
# → webhook messages, de 5511987654321, nome de perfil Maria Souza

Um template que a Meta reprova

Prova que a sua tela de templates mostra o veredito e o motivo. A revisão leva o tempo que você escolher, e o webhook de status chega como a Meta manda.

curl -X POST localhost:3010/v23.0/100000000000001/message_templates \
  -H 'Authorization: Bearer test' -H 'Content-Type: application/json' \
  -H 'X-Mock-Template-Review: REJECTED' \
  -H 'X-Mock-Template-Review-Reason: INVALID_FORMAT' \
  -H 'X-Mock-Template-Review-Delay: 5000' \
  -d '{"name":"promo_black_friday","language":"pt_BR","category":"MARKETING",
       "components":[{"type":"BODY","text":"Até 50% off hoje!"}]}'
# → PENDING; 5 s depois, webhook message_template_status_update REJECTED, motivo INVALID_FORMAT

Uma mensagem que nunca é lida, ou falha

Prova a sua lógica de follow-up para mensagens paradas em entregue, ou com falha. Com MOCK_DELIVERY_MODE=manual, os tiques só andam quando o celular do usuário dá a confirmação.

curl -X POST localhost:3010/v23.0/1234567890/messages \
  -H 'Authorization: Bearer test' -H 'Content-Type: application/json' \
  -H 'X-Mock-From: 5511987654321' -H 'X-Mock-Message-Status: failed' \
  -d '{"messaging_product":"whatsapp","status":"read","message_id":"wamid.…"}'
# → webhook de status failed para essa mensagem

O onboarding de um número novo

Prova o seu fluxo de onboarding com os passos e os erros da Meta: o código por SMS (mostrado no painel), um código errado, o PIN, um número fora da Cloud API tentando enviar.

POST /v23.0/100000000000001/phone_numbers   {"cc":"55","phone_number":"11955554444","verified_name":"Clínica"}
POST /v23.0/{phone-number-id}/request_code   # o código aparece no número, no painel
POST /v23.0/{phone-number-id}/verify_code    # código errado é (#136025)
POST /v23.0/{phone-number-id}/register       # PIN de seis dígitos; outro PIN depois é (#133005)
POST /v23.0/{phone-number-id}/settings       # ligações ativadas; precisa de webhook, senão (#138018)
POST /v23.0/{phone-number-id}/messages       # antes do register: (#133010)

Ligações com áudio de verdade

Prova a sua integração com a Calling API de ponta a ponta: o pedido de permissão e a resposta do usuário, pre_accept, accept, terminate e os webhooks de calls, com áudio WebRTC real do seu microfone, no painel.

A tela de ligação do painel com o cronômetro e os botões de mudo e desligar.

Conferindo a assinatura do webhook

Prova que o seu endpoint recusa o que a Meta não assinou. Com WEBHOOK_SECRET definido, todo webhook leva x-hub-signature-256, o HMAC-SHA256 do corpo bruto, como o app secret da Meta.

import { createHmac, timingSafeEqual } from 'node:crypto'

const expected = 'sha256=' + createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(rawBody).digest('hex')
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-hub-signature-256']))

Mídia e áudios de voz

Prova o seu fluxo de mídia: upload com os tipos e limites de tamanho da Meta, download com token, exclusão. No painel, o microfone grava um áudio de voz de verdade (Ogg Opus) e o seu webhook recebe audio com voice: true.

curl -X POST localhost:3010/v23.0/1234567890/media \
  -H 'Authorization: Bearer test' \
  -F messaging_product=whatsapp -F type=image/jpeg -F file=@foto.jpg
# → {"id":"…"}; depois envie como {"type":"image","image":{"id":"…"}}

O painel: você é o usuário.

Abra http://localhost:3010. Uma tela no estilo WhatsApp Web sem rotas privadas: cada ação é a chamada da Cloud API que a sua aplicação faria, ou uma chamada a /settings para o que a Meta não tem endpoint.