Configuracao
Variaveis de Ambiente
| Bash |
|---|
| # 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.
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 |
|---|
| 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 |
|---|
| 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'
}
]
|
| 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().
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 |
|---|
| 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