Executando verificação de segurança...
-1

Como construímos a integração com a WhatsApp Cloud API oficial da Meta — o que aprendemos

Integrar WhatsApp Cloud API oficial da Meta parece simples. Não é.

Documentação incompleta, sandbox que mente, token que expira
silenciosamente em produção. Passamos por tudo isso construindo
o RelayOS. Documentamos aqui o que aprendemos

O problema que queríamos resolver

As opções disponíveis no mercado brasileiro eram ruins:

  • Z-API / Evolution API: funciona, mas não-oficial. Vimos número ser banido em produção.
  • Twilio: caro em dólar, contrato, overhead desnecessário.
  • Construir do zero: semanas só de infra, antes de mandar a primeira mensagem.

Decidimos construir uma camada simples, confiável e oficial em cima da Meta Cloud API.

Stack

Java 21 + Spring Boot 3.4
PostgreSQL 16 + Flyway
Railway (deploy)

O que implementamos

Retry com backoff exponencial

Se a Meta estiver instável, a mensagem não se perde. Um scheduler
roda a cada 10s buscando mensagens elegíveis para reenvio.

Usamos FOR UPDATE SKIP LOCKED para garantir que funciona
em múltiplas instâncias sem precisar de Redis ou lock externo:

SELECT * FROM messages
WHERE status IN ('QUEUED', 'FAILED')
  AND next_attempt_at <= NOW()
  AND processing_started_at IS NULL
FOR UPDATE SKIP LOCKED
LIMIT 10

Backoff: 30s → 2min → 10min → 30min → 2h. Após 5 tentativas, status vira FAILED.

Idempotency-Key por header

Padrão do Stripe. Se o cliente mandar o mesmo request duas vezes
(retry de rede, bug no sistema dele), só uma mensagem é enviada.

curl -X POST https://api.relayos.com.br/v1/messages \
  -H "Authorization: Bearer rly_live_..." \
  -H "Idempotency-Key: pedido-42-lembrete" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+5511999998888",
    "template": "lembrete_consulta",
    "language": "pt_BR",
    "variables": {"1": "João", "2": "14h"}
  }'

A chave é escopada por projeto. Mesmo Idempotency-Key em projetos
diferentes não conflita.

Outbound webhooks com retry

Quando o status de uma mensagem muda (SENT → DELIVERED → READ),
fazemos um POST no callbackUrl do cliente automaticamente.

{
  "event": "message.delivered",
  "message_id": "uuid-...",
  "to": "+5511999998888",
  "template": "lembrete_consulta",
  "status": "DELIVERED",
  "occurred_at": "2026-05-14T14:22:10Z"
}

Se o endpoint do cliente falhar, reprocessamos com backoff.
O cliente também pode consultar o histórico via GET /v1/webhook-deliveries.

Bugs que nos custaram tempo

1. @Async + @Transactional na mesma classe — Spring AOP silencioso

Spring usa proxy AOP para interceptar chamadas de método.
Quando você chama this.metodo() dentro da mesma classe,
a chamada não passa pelo proxy — @Transactional é ignorado
silenciosamente, sem erro, sem aviso.

O sintoma: método marcado como @Transactional rodava sem transação.
save() não persistia. Dados sumiam.

Fix: extrair qualquer método @Transactional que precisa ser chamado
de dentro do mesmo bean para uma classe separada:

// ❌ Não funciona — this.applyStatus() ignora @Transactional
@Async
public void deliver(UUID messageId) {
    MetaSendResponse resp = metaClient.send(req);
    this.applyStatus(messageId, resp); // proxy não intercepta
}

// ✅ Funciona — bean externo passa pelo proxy AOP
@Async
public void deliver(UUID messageId) {
    MetaSendResponse resp = metaClient.send(req);
    messageStatusService.applyStatus(messageId, resp); // proxy intercepta
}

Mesma regra vale para @Scheduled + @Transactional na mesma classe.

2. Sandbox da Meta com números brasileiros

Durante os testes, mensagens retornavam 200 OK com wamid real:

{
  "messages": [{"id": "wamid.HBgL...", "message_status": "accepted"}]
}

Mas não chegavam no dispositivo. Sem erro, sem log de falha.

Depois de horas debugando, descobrimos: é uma limitação conhecida
do sandbox da Meta com alguns números brasileiros (especialmente Vivo).
O número é modificado internamente (5551991246202555191246202)
e a entrega falha silenciosamente.

Solução: usar número de produção próprio em vez do sandbox para testes reais.

3. eSIM novo já vem com WhatsApp ativo

Compramos um eSIM para vincular na Cloud API da Meta.
O número veio com WhatsApp Business pré-ativado pelo operador.

A Meta bloqueia o registro na Cloud API se o número já está
em uso no app WhatsApp:

Error: Account not registered
Code: 133010

Fix: instalar o WhatsApp no número, acessar
Configurações → Conta → Deletar minha conta,
aguardar 5 minutos e tentar o registro novamente via API:

curl -X POST \
  https://graph.facebook.com/v25.0/{phone-number-id}/register \
  -H "Authorization: Bearer {token}" \
  -d '{"messaging_product": "whatsapp", "pin": "000000"}'

4. Token temporário da Meta expira em 24h

O token gerado pelo painel do developers.facebook.com expira em 24 horas.
Em produção isso quebra tudo silenciosamente.

Solução: criar um System User no Meta Business Manager,
gerar token sem expiração com as permissões:

  • whatsapp_business_messaging
  • whatsapp_business_management

Esse token não expira e é o correto para uso em produção.

O resultado

O RelayOS está no ar em produção:

curl -X POST https://api.relayos.com.br/v1/messages \
  -H "Authorization: Bearer rly_live_..." \
  -H "Idempotency-Key: consulta-42-lembrete-24h" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+5511999998888",
    "template": "lembrete_consulta",
    "language": "pt_BR",
    "variables": {"1": "João Silva", "2": "14h"}
  }'
{
  "id": "uuid-...",
  "status": "QUEUED",
  "queuedAt": "2026-05-14T..."
}

Em ~2 segundos: QUEUED → SENT → DELIVERED → READ.

Free tier: 100 mensagens/mês, sem cartão de crédito.

Se você está integrando WhatsApp Cloud API e travou em algum
desses pontos — ou em outro que não está aqui — comenta abaixo.

relayos.com.br
docs.relayos.com.br
api.relayos.com.br/docs

Aberto a feedback técnico e perguntas sobre a implementação.

Carregando publicação patrocinada...
1