Pular para o conteúdo

WhatsApp Cloud API Integration

x17 integra com a WhatsApp Cloud API da Meta (v23.0) via Facebook Embedded Signup (modalidade Coexistente). Cada cliente conecta SEU PROPRIO número WhatsApp — a plataforma nunca usa um número global.

ArquivoDescrição
src/lib/whatsapp/meta-api.tsMetaWhatsAppApi class + convertToMetaFormat()
src/lib/whatsapp/encryption.tsAES-256-GCM encrypt/decrypt
src/lib/whatsapp/facebook-sdk.tsOAuth flow + Embedded Signup helpers
src/app/api/whatsapp/webhook/route.tsWebhook principal (590+ linhas)
src/lib/automations/whatsapp-sender.tsAbstrações de envio para automações

O frontend carrega o Facebook SDK e inicia o processo:

facebook-sdk.ts
getEmbeddedSignupUrl(redirectUri, state)
// -> https://www.facebook.com/v23.0/dialog/oauth?client_id=...&scope=whatsapp_business_management,whatsapp_business_messaging

O config_id (env FACEBOOK_CONFIG_ID) define que é um signup coexistente.

Após o usuário autorizar no Facebook:

exchangeCodeForToken(code, redirectUri)
// -> POST https://graph.facebook.com/v23.0/oauth/access_token
// -> Retorna { access_token, token_type }

O short-lived token é trocado por um long-lived token (60 dias):

exchangeForLongLivedToken(shortLivedToken)
// -> GET https://graph.facebook.com/v23.0/oauth/access_token?grant_type=fb_exchange_token
// -> Retorna { access_token, expires_at }
getWhatsAppBusinessAccounts(accessToken)
// 1. debug_token para extrair WABA IDs dos granular_scopes
// 2. Para cada WABA: buscar phone_numbers (id, display_phone_number, verified_name)
// -> Retorna WhatsAppBusinessAccount[]

Após selecionar o phone number:

  1. api.registerPhoneNumber(phoneNumberId, pin) - registra na API Cloud
  2. api.subscribeApp(wabaId) - inscreve o app para receber webhooks
  3. Salva perfil em whatsapp_profiles com token criptografado

Todos os access tokens (WhatsApp e integração) são criptografados em repouso.

  • AES-256-GCM (Galois/Counter Mode)
  • Key: env var ENCRYPTION_KEY (32 bytes em hex = 64 hex chars)
  • IV: 12 bytes random por encriptação
{iv_hex}:{authTag_hex}:{encrypted_hex}

Exemplo: a1b2c3d4e5f6a1b2c3d4e5f6:deadbeefdeadbeefdeadbeefdeadbee:cafebabe...

encrypt(plaintext: string): string // Retorna iv:authTag:encrypted
decrypt(ciphertext: string): string // Retorna plaintext
  • encrypt(): ao salvar access_token no banco (Embedded Signup, OAuth)
  • decrypt(): ao ler access_token para enviar mensagens ou chamar API Meta

Client HTTP para a WhatsApp Cloud API.

new MetaWhatsAppApi({ accessToken, timeout?: 30000 })
MétodoDescriçãoEndpoint
sendMessage(phoneNumberId, payload)Envia mensagem (texto, template, etc.)POST /{phoneNumberId}/messages
submitTemplate(wabaId, template)Submete template para aprovaçãoPOST /{wabaId}/message_templates
listTemplates(wabaId)Lista templatesGET /{wabaId}/message_templates
deleteTemplate(wabaId, name)Deleta templateDELETE /{wabaId}/message_templates?name=...
registerPhoneNumber(phoneNumberId, pin)Registra phone numberPOST /{phoneNumberId}/register
subscribeApp(wabaId)Inscreve app para webhooksPOST /{wabaId}/subscribed_apps
getSubscribedApps(wabaId)Lista apps inscritosGET /{wabaId}/subscribed_apps
getPhoneNumber(phoneNumberId)Detalhes do phone numberGET /{phoneNumberId}?fields=...
https://graph.facebook.com/v23.0
class MetaApiError extends Error {
statusCode: number // HTTP status (400, 401, 403, etc.)
details: unknown // JSON response da Meta
}

Converte um MessageTemplate do x17 para o formato MetaTemplatePayload da API Meta.

Mapeamento de componentes:

  • header_type != “none” -> HEADER component (TEXT, IMAGE, VIDEO, DOCUMENT, LOCATION)
  • body_text -> BODY component + example.body_text se houver variaveis
  • footer_text -> FOOTER component
  • buttons[] -> BUTTONS component

Mapeamento de botoes:

x17 typeMeta type
urlURL
phonePHONE_NUMBER
quick_replyQUICK_REPLY
copy_codeCOPY_CODE
otpOTP

Template types especiais:

  • carousel: componente CAROUSEL com cards[] (header + body + buttons por card)
  • lto (Limited Time Offer): componente LIMITED_TIME_OFFER com expiração
  • authentication: campos add_security_recommendation e code_expiration_minutes no body

O webhook é o endpoint que a Meta chama para notificar sobre mensagens recebidas, status de mensagens, e atualizações de templates.

GET /api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=...&hub.challenge=...

Retorna o challenge se o verify_token confere com WHATSAPP_VERIFY_TOKEN.

Autenticação:

  1. Rate limiting: 100 requests/minuto por IP
  2. Verificação de FACEBOOK_APP_SECRET configurado
  3. Validação HMAC-SHA256 do payload (x-hub-signature-256 header)
    const expected = "sha256=" + createHmac("sha256", appSecret).update(rawBody).digest("hex")
    timingSafeEqual(sigBuf, expBuf)

Processamento: Itera sobre body.entry[].changes[] e processa por field:

  1. Extrair phone_number_id do metadata
  2. Lookup perfil: Busca whatsapp_profiles com phone_number_id e status: connected
    • Usa .order("created_at", { ascending: false }).limit(1) para evitar erro com múltiplos perfis
  3. Processar mensagens inbound (value.messages[]): a. Normalizar telefone (adicionar + se necessário) b. Upsert contato: contacts com UNIQUE(account_id, phone) e ignoreDuplicates: true c. Upsert conversa: buscar existente ou criar nova, reabrir se fechada d. Inserir mensagem: com wa_message_id (partial unique index para idempotencia) e. emitEvent(“Message Received”) f. dispatchTrigger(“message_received”) com:
    • contact_id, message (raw), message_text, from, timestamp, message_type, conversation_id g. Se novo contato: dispatchTrigger(“new_contact”) h. handleAutoReply(): chatbot IA autonomo (fire-and-forget)
  4. Processar status updates (value.statuses[]): a. Atualizar messages.status para sent/delivered/read/failed b. Se failed: salvar error.code e error.title no content c. Emitir eventos: Message Delivered, Message Read, Message Failed, Template Delivered, Template Read

Processa mudancas de status de templates (aprovação, rejeicao, etc.):

  1. Mapear evento Meta -> status x17:
    APPROVED -> approved
    REJECTED -> rejected
    PAUSED -> paused
    DISABLED -> disabled
    FLAGGED -> paused
    REINSTATED -> approved
    PENDING_DELETION -> disabled
  2. Buscar perfil por waba_id
  3. Atualizar template no banco (por meta_template_id ou por name + language)
  4. dispatchTrigger(“template_status_changed”)
  5. Criar notificação para o usuário (sucesso/erro/info)
function extractMessageContent(msg)
TipoConteúdo extraido
text{ type: "text", body: msg.text.body }
image/video/document{ type, media_id, caption }
interactive{ type: "interactive", ...interactive }
outros{ type, raw: msg }

O webhook é um UNICO endpoint para TODOS os clientes. O roteamento funciona assim:

Meta envia payload com:
entry[0].changes[0].value.metadata.phone_number_id = "123456789"
Query:
SELECT account_id FROM whatsapp_profiles
WHERE phone_number_id = '123456789'
AND status = 'connected'
ORDER BY created_at DESC
LIMIT 1
Resultado: account_id = "abc-123" -> processar nesse contexto

Se o perfil não for encontrado, a mensagem e ignorada com log de erro.

whatsapp-sender.ts
sendWhatsAppText(accountId, to, text)
-> getWhatsAppProfile(accountId) // busca perfil + decrypt token
-> api.sendMessage(phoneNumberId, { to, type: "text", text: { body } })
sendWhatsAppTemplate(accountId, to, templateName, language, parameters, buttonComponents?)
-> getWhatsAppProfile(accountId)
-> api.sendMessage(phoneNumberId, {
to,
type: "template",
template: {
name: templateName,
language: { code: language },
components: [
{ type: "body", parameters: [{ type: "text", text: "valor" }] },
...buttonComponents
]
}
})
id UUID PK
account_id UUID FK -> accounts
name TEXT (verified_name)
phone_number TEXT (display_phone_number)
waba_id TEXT (WhatsApp Business Account ID)
phone_number_id TEXT (Phone Number ID na API Meta)
access_token_encrypted TEXT (AES-256-GCM)
status: connected | disconnected
created_at TIMESTAMPTZ
WHATSAPP_API = {
GRAPH_VERSION: "v23.0",
GRAPH_BASE_URL: "https://graph.facebook.com",
FB_SDK_URL: "https://connect.facebook.net/en_US/sdk.js",
}
FACEBOOK_APP_ID # App ID do Facebook
FACEBOOK_APP_SECRET # App Secret (para HMAC webhook)
FACEBOOK_CONFIG_ID # Config ID do Embedded Signup
WHATSAPP_VERIFY_TOKEN # Token de verificação do webhook
ENCRYPTION_KEY # Chave AES-256 em hex (64 chars)