Appearance
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:
| Estado | Sinal | Comportamento do bot |
|---|---|---|
| Não vinculado | account === null | Pede identificador / valida OTP |
| Empresa pendente | account.defaultCompanyId === null | Lista empresas / processa escolha |
| Vinculado | account.defaultCompanyId !== null | Comandos (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ão | Detector | Ação |
|---|---|---|
| 6 dígitos exatos | /^\d{6}$/ | verifyOtp |
Contém @ e tem TLD | /^[^\s@]+@[^\s@]+\.[^\s@]+$/ | issueOtp (email) |
| 11 dígitos depois de strip | length === 11 | issueOtp (CPF) |
| 14 dígitos depois de strip | length === 14 | issueOtp (CNPJ) |
| Outro | REPLY_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.codeHasharmazenasha256(code). O código cleartext nunca persiste. - TTL de 10 minutos:
expiresAtno momento do issue. Issue novo invalida pendentes do mesmo número. - 5 tentativas: a cada
verifyerrado,attemptsincrementa. Chegando em 5, qualquer verify retorna403até 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 horasstep by step) - Notificações outbound (API → bot → WhatsApp)
Documento atualizado em Maio 2026