Skip to content

Fluxo de Connect (OTP)

Como um usuário vincula o número de WhatsApp à conta Contrasync. Todo o fluxo acontece no chat do WhatsApp, não existe página web para isso.


Estados da WhatsappAccount

O bot é stateless. Toda decisão é derivada do estado da WhatsappAccount retornado pela API:

EstadoSinalComportamento do bot
Não vinculadoaccount === nullPede identificador / valida OTP
Empresa pendenteaccount.defaultCompanyId === nullLista empresas / processa escolha
Vinculadoaccount.defaultCompanyId !== nullComandos (status, trocar empresa, etc.)

Diagrama de sequência (caminho feliz, múltiplas empresas)

USER          BOT (Lambda)           NEST-API
 │                │                     │
 │  "oi"          │                     │
 │───────────────►│                     │
 │                │ checkIdempotency    │
 │                │────────────────────►│
 │                │◄────────────────────│ { duplicate: false }
 │                │ getAccountByPhone   │
 │                │────────────────────►│
 │                │◄────────────────────│ null
 │                │                     │
 │ REPLY_HELP_UNLINKED                  │
 │◄───────────────│                     │
 │                │                     │
 │ "[email protected]"   │                     │
 │───────────────►│                     │
 │                │ checkIdempotency    │
 │                │────────────────────►│
 │                │◄────────────────────│ { duplicate: false }
 │                │ getAccountByPhone   │
 │                │────────────────────►│
 │                │◄────────────────────│ null
 │                │ issueOtp            │
 │                │────────────────────►│ (envia email)
 │                │◄────────────────────│ { emailSentTo: "jo***@x.com" }
 │ REPLY_OTP_SENT │                     │
 │◄───────────────│                     │
 │                │                     │
 │ "123456"       │                     │
 │───────────────►│                     │
 │                │ checkIdempotency    │
 │                │────────────────────►│
 │                │◄────────────────────│ { duplicate: false }
 │                │ getAccountByPhone   │
 │                │────────────────────►│
 │                │◄────────────────────│ null
 │                │ verifyOtp           │
 │                │────────────────────►│ (cria WhatsappAccount,
 │                │                     │  defaultCompanyId=null)
 │                │◄────────────────────│ { account, companies: [A, B] }
 │ REPLY_PICK_COMPANY                   │
 │◄───────────────│                     │
 │                │                     │
 │ "1"            │                     │
 │───────────────►│                     │
 │                │ checkIdempotency    │
 │                │────────────────────►│
 │                │◄────────────────────│ { duplicate: false }
 │                │ getAccountByPhone   │
 │                │────────────────────►│
 │                │◄────────────────────│ { defaultCompanyId: null }
 │                │ listCompanies       │
 │                │────────────────────►│
 │                │◄────────────────────│ [A, B]
 │                │ switchCompany A     │
 │                │────────────────────►│
 │                │◄────────────────────│ { ... }
 │ REPLY_COMPANY_SELECTED("Acme")       │
 │◄───────────────│                     │

Para usuário com uma única empresa, o verifyOtp já preenche defaultCompanyId e o bot pula direto para REPLY_COMPANY_SELECTED.


Detector de input no estado "não vinculado"

BotRouterService.handleUnlinked() distingue input pelo padrão do texto:

PadrãoDetectorAção
6 dígitos exatos/^\d{6}$/verifyOtp
Contém @ e tem TLD/^[^\s@]+@[^\s@]+\.[^\s@]+$/issueOtp (email)
11 dígitos depois de striplength === 11issueOtp (CPF)
14 dígitos depois de striplength === 14issueOtp (CNPJ)
OutroREPLY_HELP_UNLINKED

Texto vazio (mídia sem caption, áudio puro) cai em REPLY_HELP_UNLINKED.


Exemplos de interação real

Conexão direta (1 empresa)

👤 oi
🤖 Olá! Sou o bot do Contrasync. Pra começar, envie seu email, CPF
   ou CNPJ cadastrado na plataforma. Vou te enviar um código de
   6 dígitos por email.

👤 [email protected]
🤖 Enviei um código de 6 dígitos para jo***@acme.com. Me responda
   com ele aqui.

👤 482917
🤖 Pronto! Você está operando como Acme Consultoria. Os comandos do
   bot ainda estão em construção  em breve você poderá lançar horas,
   ver compliance e mais.

Conexão com múltiplas empresas

👤 12345678909
🤖 Enviei um código de 6 dígitos para jo***@acme.com. Me responda
   com ele aqui.

👤 730284
🤖 Em qual empresa você quer operar? Responda com o número:
     1. Acme Consultoria
     2. Beta Tecnologia
     3. Gama Estúdio

👤 2
🤖 Pronto! Você está operando como Beta Tecnologia. Os comandos do
   bot ainda estão em construção  em breve você poderá lançar horas,
   ver compliance e mais.

Erros vindos da API são repassados

👤 [email protected]
🤖 Nenhum usuário encontrado para esse email.

👤 12345678000190
🤖 Mais de um usuário nessa empresa. Informe seu email ou CPF.

👤 999999
🤖 Código inválido.

👤 999999  (5ª tentativa errada)
🤖 Limite de tentativas atingido. Solicite um novo código.

Comandos depois de vinculado

👤 status
🤖 👤 João Silva ([email protected])
   🏢 Empresa atual: Acme Consultoria
   
   Comandos disponíveis: trocar empresa, status

👤 trocar empresa
🤖 Em qual empresa você quer operar? Responda com o número:
     1. Acme Consultoria
     2. Beta Tecnologia

👤 trocar empresa 2
🤖 Pronto! Você está operando como Beta Tecnologia. ...

Logs gerados durante o connect

Sequência típica (3 invocations: email → OTP → escolha) em um único whatsappNumber:

json
{"level":"INFO","logger":"BotRouterService","message":"Routing reply","phone":"5511999998888"}
{"level":"INFO","logger":"BotRouterService","message":"Routing reply","phone":"5511999998888"}
{"level":"INFO","logger":"BotRouterService","message":"Routing reply","phone":"5511999998888"}

Erro 4xx da API (forwarded ao usuário, não é exception):

json
{"level":"WARN","logger":"ContrasyncApiClient","message":"Falha em issueOtp: Nenhum usuário encontrado para esse email.","status":404}
{"level":"INFO","logger":"BotRouterService","message":"Routing reply","phone":"5511999998888"}

OTP duplicado (Z-API retransmitiu):

json
{"level":"INFO","logger":"InboundWebhookHandler","message":"Mensagem duplicada ignorada","messageId":"MSG-3F2A"}

Segurança

  • OTP em hash: whatsapp_otps.codeHash armazena sha256(code). O código cleartext nunca persiste.
  • TTL de 10 minutos: expiresAt no momento do issue. Issue novo invalida pendentes do mesmo número.
  • 5 tentativas: a cada verify errado, attempts incrementa. Chegando em 5, qualquer verify retorna 403 até issue novo.
  • Email como canal: garante que mesmo se o número do WhatsApp foi sequestrado, o atacante não recebe o OTP. A janela é o tempo entre issueOtp (atacante já dentro do WhatsApp) e a vítima ver o email.

O que não está coberto (Fase 4+)

  • Desvincular conta (desconectar): hoje precisa via banco
  • Trocar email: hoje precisa pelo app web
  • Sessões multi-turno para comandos complexos (lançar horas step by step)
  • Notificações outbound (API → bot → WhatsApp)

Documento atualizado em Maio 2026