Pular para conteúdo

Integracao Meta WhatsApp Business Cloud API

Configuracao

Variaveis de Ambiente

Bash
1
2
3
4
5
6
7
# backend/.env
META_PHONE_NUMBER_ID=984930428037369   # ID do numero no Meta Business
META_BUSINESS_ID=1653394962318745       # Business Account ID
META_ACCESS_TOKEN=EAAxxxxx...           # Access Token (60 dias)
META_API_VERSION=v22.0
META_VERIFY_TOKEN=my_verify_token       # Para verificar webhook
META_APP_SECRET=                        # PENDENTE - para HMAC

Renovacao do Access Token

Tokens duram 60 dias. Renovar em: https://developers.facebook.com/apps/<APP_ID>/whatsapp-business/wa-dev-console/

Apos atualizar .env:

Bash
# IMPORTANTE: restart NAO recarrega .env, precisa force-recreate
docker compose up -d --force-recreate backend celery_worker celery_beat

Verificacao:

Bash
docker compose exec celery_worker printenv | grep META_ACCESS_TOKEN


Webhook

Endpoints

Metodo URL Funcao
GET /api/v1/webhook/meta/ Verificacao inicial pela Meta
POST /api/v1/webhook/meta/ Recebimento de mensagens e status

Verificacao (GET)

Meta envia GET com query params: - hub.mode=subscribe - hub.verify_token=<token> - hub.challenge=<random>

A view valida verify_token == META_VERIFY_TOKEN e responde com challenge em plain text.

Processamento de mensagens (POST)

Payload tipico:

JSON
{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "1653394962318745",
    "changes": [{
      "field": "messages",
      "value": {
        "messaging_product": "whatsapp",
        "metadata": {
          "phone_number_id": "984930428037369",
          "display_phone_number": "551138719041"
        },
        "contacts": [{
          "wa_id": "5511999999999",  // ATENCAO: BR pode vir sem o "9"
          "profile": {"name": "Joao Silva"}
        }],
        "messages": [{
          "id": "wamid.HBg...",
          "from": "5511999999999",
          "type": "text" | "button" | "interactive" | "image" | "audio" | ...,
          "timestamp": "1770938856",
          // campo varia por type:
          "text": {"body": "Sim"},
          "button": {"text": "Sim", "payload": "Sim"},
          "interactive": {
            "type": "button_reply" | "list_reply",
            "button_reply": {"id": "opt_xxx", "title": "Sim"}
          }
        }]
      }
    }]
  }]
}

Processamento de status updates

Meta tambem envia atualizacoes de status das mensagens enviadas:

JSON
{
  "object": "whatsapp_business_account",
  "entry": [{
    "changes": [{
      "field": "messages",
      "value": {
        "statuses": [{
          "id": "wamid....",
          "status": "sent" | "delivered" | "read" | "failed",
          "timestamp": "1770938860",
          "recipient_id": "5511999999999"
        }]
      }
    }]
  }]
}

A view atualiza MessageLog.status via payload__message_id lookup, respeitando hierarquia: failed < sent < delivered < read.


Servico MetaGraphService

backend/whatsapp/services/meta_graph_service.py

Encapsula todas as chamadas a Graph API.

Metodos principais

Python
class MetaGraphService:
    def send_message(phone, message) -> tuple[bool, str | None]:
        """Texto simples (so dentro da janela 24h)."""

    def send_template_message(phone, template_name, language_code, components) -> tuple[bool, str | None]:
        """Template aprovado (pode iniciar conversa)."""

    def send_interactive_message(phone, text, buttons, title) -> tuple[bool, str | None]:
        """Botoes de resposta rapida (max 3)."""

    def send_list_message(phone, text, button_text, sections, title) -> tuple[bool, str | None]:
        """Lista/menu com ate 10 opcoes."""

    def send_initial_survey_message(survey_run, send_first_question, use_buttons) -> bool:
        """Envia convite inicial da pesquisa."""

    def send_question_message(survey_run, question) -> bool:
        """Envia uma pergunta especifica."""

    def send_completion_message(survey_run, custom_message) -> bool:
        """Envia mensagem final apos completar."""

Normalizacao de telefone

_normalize_phone(phone) remove caracteres nao-numericos antes de enviar para a Meta:

Python
"+55 (11) 99999-9999" -> "5511999999999"

Tratamento de erros

Hoje captura requests.RequestException genericamente:

Python
1
2
3
4
5
6
7
8
9
try:
    response = requests.post(url, json=payload, headers=headers, timeout=30)
    response.raise_for_status()
except requests.RequestException as e:
    logger.error(f'Falha ao enviar para {phone}: {str(e)}')
    if hasattr(e, 'response') and e.response is not None:
        logger.error(f'Status code: {e.response.status_code}')
        logger.error(f'Response body: {e.response.text}')
    return (False, None)

Melhoria pendente: parsear e.response.json()['error']['code'] e tomar acoes especificas (vide roadmap-gaps.md#gap-3).


Problema do "9" do Celular Brasileiro

A Meta nem sempre envia o wa_id com o digito "9" do celular brasileiro, mesmo quando o numero esta cadastrado com ele.

Exemplo: - Cadastrado: 5534988682409 (13 digitos) - Webhook recebe: 553488682409 (12 digitos)

Solucao: backend/whatsapp/utils.py

Python
1
2
3
4
5
def get_phone_variants_br(phone: str) -> List[str]:
    """Gera variantes com/sem o '9' para numeros BR."""

def find_contact_by_phone(phone: str) -> tuple[Contact, str]:
    """Busca contato tolerando variacao do '9'."""

Usado em meta_webhook e evolution_webhook para identificar corretamente o contato e a SurveyRun ativa.


Templates

Template highlas (exemplo do cliente)

Configuracao em Survey.whatsapp_template_*:

Python
whatsapp_template_name = 'highlas'
whatsapp_template_language = 'pt_BR'
whatsapp_template_params = [
    {
        'type': 'header',
        'field': 'first_name',     # busca em Contact.name (primeira palavra)
        'index': 0,
        'paramType': 'text'
    }
]

Payload enviado para Meta

JSON
{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "5511999999999",
  "type": "template",
  "template": {
    "name": "highlas",
    "language": {
      "code": "pt_BR",
      "policy": "deterministic"
    },
    "components": [{
      "type": "header",
      "parameters": [{
        "type": "text",
        "text": "Joao"
      }]
    }]
  }
}

Categorias e Restricoes

Vide diretrizes-meta.md.


Janela 24h (Customer Service Window)

Cenario Pode enviar? Como
Cliente enviou msg ha 1h ✅ Sim Qualquer tipo (texto, midia, interactive)
Cliente enviou msg ha 23h59m ✅ Sim Qualquer tipo
Cliente enviou msg ha 24h01m ❌ Nao Apenas template aprovado
Cliente nunca enviou msg ❌ Nao Apenas template aprovado

Bloqueio implementado em chat/services.py:ChatService._validate_can_send().


Custos da Meta

Modelo de conversation-based pricing:

  • User-initiated conversation (cliente inicia): GRATIS por 1 sessao de 24h
  • Business-initiated conversation (template): pago, ~R$0,06-0,15 por sessao 24h

Templates AUTHENTICATION: ~R\(0,03 Templates UTILITY: ~R\)0,06 Templates MARKETING: ~R$0,15

Implicacao para o sistema: - Cada start_survey que envia template = custo - Reenvio apos expiracao = novo custo - Por isso o cooldown de reenvio e importante (vide roadmap)


Debugging

Ver payload completo de um webhook recente

Bash
1
2
3
4
5
6
docker compose exec backend python manage.py shell -c "
from whatsapp.models import MessageLog
log = MessageLog.objects.filter(direction='in').order_by('-created_at').first()
import json
print(json.dumps(log.payload, indent=2))
"

Simular webhook localmente

Bash
curl -X POST https://highlasdobrasil.com.br/api/v1/webhook/meta/ \
  -H "Content-Type: application/json" \
  -d '{
    "object": "whatsapp_business_account",
    "entry": [{
      "changes": [{
        "field": "messages",
        "value": {
          "messaging_product": "whatsapp",
          "metadata": {"phone_number_id": "984930428037369"},
          "messages": [{
            "id": "wamid.TESTE",
            "from": "5511999999999",
            "type": "text",
            "timestamp": "1770000000",
            "text": {"body": "Sim"}
          }]
        }
      }]
    }]
  }'

Ver requisicoes recentes ao webhook

Bash
docker compose logs backend --tail 200 | grep "webhook/meta"

Status do token

Bash
TOKEN=$(grep META_ACCESS_TOKEN backend/.env | cut -d= -f2)
curl -sX GET "https://graph.facebook.com/v22.0/me?access_token=$TOKEN"

Se retornar OAuthException -> token expirou ou foi revogado.


Limites Conhecidos

Limites de Frequencia

A Meta tem tiers de mensageria baseados em quality rating: - TIER 1000: ate 1.000 conversation/24h - TIER 10K: ate 10.000 - TIER 100K: ate 100.000 - TIER UNLIMITED: sem limite

O cliente Highlas esta no TIER inicial (verificar no Business Manager).

Limites de UI

  • Botoes interactive: max 3
  • Itens em lista: max 10
  • Texto de botao: max 20 caracteres
  • Texto de header: max 60 caracteres

Validacoes em meta_graph_service.py cortam textos automaticamente.

Tamanho de midia

  • Imagem: 5MB
  • Audio: 16MB
  • Video: 16MB
  • Documento: 100MB

Validacao em chat/services.py:send_media.


Referencias