WhatsApp Cloud API Integration
Visão Geral
Seção intitulada “Visão Geral”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.
Arquivos
Seção intitulada “Arquivos”| Arquivo | Descrição |
|---|---|
src/lib/whatsapp/meta-api.ts | MetaWhatsAppApi class + convertToMetaFormat() |
src/lib/whatsapp/encryption.ts | AES-256-GCM encrypt/decrypt |
src/lib/whatsapp/facebook-sdk.ts | OAuth flow + Embedded Signup helpers |
src/app/api/whatsapp/webhook/route.ts | Webhook principal (590+ linhas) |
src/lib/automations/whatsapp-sender.ts | Abstrações de envio para automações |
Facebook Embedded Signup Flow
Seção intitulada “Facebook Embedded Signup Flow”1. Início do Signup
Seção intitulada “1. Início do Signup”O frontend carrega o Facebook SDK e inicia o processo:
getEmbeddedSignupUrl(redirectUri, state)// -> https://www.facebook.com/v23.0/dialog/oauth?client_id=...&scope=whatsapp_business_management,whatsapp_business_messagingO config_id (env FACEBOOK_CONFIG_ID) define que é um signup coexistente.
2. Troca de Código por Token
Seção intitulada “2. Troca de Código por Token”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 }3. Long-Lived Token
Seção intitulada “3. Long-Lived Token”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 }4. Descoberta de WABA e Phone Numbers
Seção intitulada “4. Descoberta de WABA e Phone Numbers”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[]5. Registro e Conexão
Seção intitulada “5. Registro e Conexão”Após selecionar o phone number:
api.registerPhoneNumber(phoneNumberId, pin)- registra na API Cloudapi.subscribeApp(wabaId)- inscreve o app para receber webhooks- Salva perfil em
whatsapp_profilescom token criptografado
Token Encryption (encryption.ts)
Seção intitulada “Token Encryption (encryption.ts)”Todos os access tokens (WhatsApp e integração) são criptografados em repouso.
Algoritmo
Seção intitulada “Algoritmo”- 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
Formato do Ciphertext
Seção intitulada “Formato do Ciphertext”{iv_hex}:{authTag_hex}:{encrypted_hex}Exemplo: a1b2c3d4e5f6a1b2c3d4e5f6:deadbeefdeadbeefdeadbeefdeadbee:cafebabe...
Funcoes
Seção intitulada “Funcoes”encrypt(plaintext: string): string // Retorna iv:authTag:encrypteddecrypt(ciphertext: string): string // Retorna plaintextencrypt(): ao salvar access_token no banco (Embedded Signup, OAuth)decrypt(): ao ler access_token para enviar mensagens ou chamar API Meta
MetaWhatsAppApi (meta-api.ts)
Seção intitulada “MetaWhatsAppApi (meta-api.ts)”Client HTTP para a WhatsApp Cloud API.
Construtor
Seção intitulada “Construtor”new MetaWhatsAppApi({ accessToken, timeout?: 30000 })Métodos
Seção intitulada “Métodos”| Método | Descrição | Endpoint |
|---|---|---|
sendMessage(phoneNumberId, payload) | Envia mensagem (texto, template, etc.) | POST /{phoneNumberId}/messages |
submitTemplate(wabaId, template) | Submete template para aprovação | POST /{wabaId}/message_templates |
listTemplates(wabaId) | Lista templates | GET /{wabaId}/message_templates |
deleteTemplate(wabaId, name) | Deleta template | DELETE /{wabaId}/message_templates?name=... |
registerPhoneNumber(phoneNumberId, pin) | Registra phone number | POST /{phoneNumberId}/register |
subscribeApp(wabaId) | Inscreve app para webhooks | POST /{wabaId}/subscribed_apps |
getSubscribedApps(wabaId) | Lista apps inscritos | GET /{wabaId}/subscribed_apps |
getPhoneNumber(phoneNumberId) | Detalhes do phone number | GET /{phoneNumberId}?fields=... |
Base URL
Seção intitulada “Base URL”https://graph.facebook.com/v23.0MetaApiError
Seção intitulada “MetaApiError”class MetaApiError extends Error { statusCode: number // HTTP status (400, 401, 403, etc.) details: unknown // JSON response da Meta}convertToMetaFormat(template)
Seção intitulada “convertToMetaFormat(template)”Converte um MessageTemplate do x17 para o formato MetaTemplatePayload da API Meta.
Mapeamento de componentes:
header_type!= “none” ->HEADERcomponent (TEXT, IMAGE, VIDEO, DOCUMENT, LOCATION)body_text->BODYcomponent +example.body_textse houver variaveisfooter_text->FOOTERcomponentbuttons[]->BUTTONScomponent
Mapeamento de botoes:
| x17 type | Meta type |
|---|---|
url | URL |
phone | PHONE_NUMBER |
quick_reply | QUICK_REPLY |
copy_code | COPY_CODE |
otp | OTP |
Template types especiais:
carousel: componenteCAROUSELcomcards[](header + body + buttons por card)lto(Limited Time Offer): componenteLIMITED_TIME_OFFERcom expiraçãoauthentication: camposadd_security_recommendationecode_expiration_minutesno body
Webhook WhatsApp (/api/whatsapp/webhook/route.ts)
Seção intitulada “Webhook WhatsApp (/api/whatsapp/webhook/route.ts)”O webhook é o endpoint que a Meta chama para notificar sobre mensagens recebidas, status de mensagens, e atualizações de templates.
GET - Verificação
Seção intitulada “GET - Verificação”GET /api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=...&hub.challenge=...Retorna o challenge se o verify_token confere com WHATSAPP_VERIFY_TOKEN.
POST - Processamento
Seção intitulada “POST - Processamento”Autenticação:
- Rate limiting: 100 requests/minuto por IP
- Verificação de
FACEBOOK_APP_SECRETconfigurado - Validação HMAC-SHA256 do payload (
x-hub-signature-256header)const expected = "sha256=" + createHmac("sha256", appSecret).update(rawBody).digest("hex")timingSafeEqual(sigBuf, expBuf)
Processamento:
Itera sobre body.entry[].changes[] e processa por field:
field: “messages”
Seção intitulada “field: “messages””- Extrair phone_number_id do
metadata - Lookup perfil: Busca
whatsapp_profilescomphone_number_idestatus: connected- Usa
.order("created_at", { ascending: false }).limit(1)para evitar erro com múltiplos perfis
- Usa
- Processar mensagens inbound (
value.messages[]): a. Normalizar telefone (adicionar+se necessário) b. Upsert contato:contactscomUNIQUE(account_id, phone)eignoreDuplicates: truec. Upsert conversa: buscar existente ou criar nova, reabrir se fechada d. Inserir mensagem: comwa_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_idg. Se novo contato: dispatchTrigger(“new_contact”) h. handleAutoReply(): chatbot IA autonomo (fire-and-forget)
- Processar status updates (
value.statuses[]): a. Atualizarmessages.statusparasent/delivered/read/failedb. Sefailed: salvarerror.codeeerror.titleno content c. Emitir eventos:Message Delivered,Message Read,Message Failed,Template Delivered,Template Read
field: “message_template_status_update”
Seção intitulada “field: “message_template_status_update””Processa mudancas de status de templates (aprovação, rejeicao, etc.):
- Mapear evento Meta -> status x17:
APPROVED -> approvedREJECTED -> rejectedPAUSED -> pausedDISABLED -> disabledFLAGGED -> pausedREINSTATED -> approvedPENDING_DELETION -> disabled
- Buscar perfil por
waba_id - Atualizar template no banco (por
meta_template_idou porname + language) - dispatchTrigger(“template_status_changed”)
- Criar notificação para o usuário (sucesso/erro/info)
Extração de Conteúdo de Mensagem
Seção intitulada “Extração de Conteúdo de Mensagem”function extractMessageContent(msg)| Tipo | Conteúdo extraido |
|---|---|
text | { type: "text", body: msg.text.body } |
image/video/document | { type, media_id, caption } |
interactive | { type: "interactive", ...interactive } |
| outros | { type, raw: msg } |
Webhook Routing Multi-Tenant
Seção intitulada “Webhook Routing Multi-Tenant”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 contextoSe o perfil não for encontrado, a mensagem e ignorada com log de erro.
Envio de Mensagens
Seção intitulada “Envio de Mensagens”Texto Livre
Seção intitulada “Texto Livre”sendWhatsAppText(accountId, to, text) -> getWhatsAppProfile(accountId) // busca perfil + decrypt token -> api.sendMessage(phoneNumberId, { to, type: "text", text: { body } })Template
Seção intitulada “Template”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 ] } })Modelo de Dados
Seção intitulada “Modelo de Dados”whatsapp_profiles
Seção intitulada “whatsapp_profiles”id UUID PKaccount_id UUID FK -> accountsname 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 | disconnectedcreated_at TIMESTAMPTZConstantes
Seção intitulada “Constantes”WHATSAPP_API = { GRAPH_VERSION: "v23.0", GRAPH_BASE_URL: "https://graph.facebook.com", FB_SDK_URL: "https://connect.facebook.net/en_US/sdk.js",}Environment Variables
Seção intitulada “Environment Variables”FACEBOOK_APP_ID # App ID do FacebookFACEBOOK_APP_SECRET # App Secret (para HMAC webhook)FACEBOOK_CONFIG_ID # Config ID do Embedded SignupWHATSAPP_VERIFY_TOKEN # Token de verificação do webhookENCRYPTION_KEY # Chave AES-256 em hex (64 chars)