Referência Técnica — Sistema de Prospecção de Representantes¶
IDs, credenciais, SQL, configs e snippets para operar o sistema.
IDs dos Workflows n8n¶
| Workflow | ID n8n | Status | Trigger | Nós |
|---|---|---|---|---|
| WF-Rep-Import (Google Sheet) | cxpqpgjSPIEX18AL |
Ativo (manual) | Manual Trigger | — |
| WF-Rep-0 — Ignição (Webhook) | I425eBF09Khja5vL |
Inativo | Webhook POST | — |
| WF-Rep-1 — Dispatcher | cstgB6ONs7R0d01r |
ATIVO | 2 Crons (8h + 14h BRT, seg-sáb) | 5 |
| WF-Rep-Worker | KrMlipmgxuc2EdlL |
ATIVO | Webhook GET /webhook/rep-worker |
31 |
| WF-Rep-Optout | IGBDGuIDLMpfCX8O |
ATIVO | Webhook GET /webhook/rep-optout |
3 |
| _TEMP_Criar_Tabela | jw5weRBzH0TBbbHd |
Arquivável | Manual (one-shot) | — |
| _TEMP_Probe (utilitário SQL) | toquIjzNhIsNN59O |
Utilitário | Manual | — |
URL base n8n: https://n8n.bepex.com.br
Credenciais no n8n¶
| Nome no n8n | ID | Tipo | Uso |
|---|---|---|---|
| Postgres n8n v2 | 7F3gxDoPsQGjAbOW |
PostgreSQL | Banco de dados representantes |
| AWS account | kqmbhAWS0tyL5fuI |
AWS | Amazon SES (us-east-2) |
| Chatwoot Bepex (Header Auth) | FzLnFD8cohSYSYIN |
Header Auth | API Chatwoot chat.bepex.com.br account 2 |
Configuração SENDERS (3 slots)¶
Array definido em dois Code nodes que devem ser mantidos sincronizados: 1. Code node de round-robin no WF-Rep-0 (ou WF-Rep-Import) 2. Code node "Roteia por Canal" no WF-Rep-Worker
const SENDERS = [
{
id: 'A',
whatsapp_inbox_id: 62, // Prospecção 1
from_email: 'andrei.mazer@bepexmkt.com.br'
},
{
id: 'B',
whatsapp_inbox_id: 63, // Prospecção 2
from_email: 'andrei@bepexmkt.com.br'
},
{
id: 'C',
whatsapp_inbox_id: 64, // Prospecção 3
from_email: 'a.mazer@bepexmkt.com.br'
}
];
Para adicionar slot D: criar caixa de email + inbox Chatwoot, adicionar objeto ao array em ambos os Code nodes (WF-Rep-0/Import e WF-Rep-Worker).
SEQUENCE_MAP (cadência)¶
const SEQUENCE_MAP = {
0: { canal: 'email', assunto: 'Oportunidade: rede de agentes comerciais Bepex em [Estado]' },
3: { canal: 'email', assunto: 'Re: Rede de agentes Bepex — comissão sobre resultado + parceria com arquitetos' },
7: { canal: 'whatsapp', template: 'whatsapp_dia7' },
14: { canal: 'whatsapp', template: 'whatsapp_dia14' },
21: { canal: 'whatsapp', template: 'whatsapp_dia21' }, // Catálogo — seg 09/jun (adicionado 2026-06-03)
22: { canal: 'email', assunto: 'Catálogo Bepex — veja os projetos antes de qualquer conversa' } // Catálogo — ter 10/jun
};
Cálculo do próximo dia:
const days = Object.keys(SEQUENCE_MAP).map(Number).sort((a,b) => a-b);
const currentIndex = days.indexOf(sequence_day);
const next_sequence_day = currentIndex < days.length - 1 ? days[currentIndex + 1] : null;
const daysUntilNext = next_sequence_day !== null ? next_sequence_day - sequence_day : null;
Amazon SES — Padrão de envio (CRÍTICO)¶
Bug do n8n: HTTP Request node com AWS predefined credential double-encodes @ quando usa contentType: "form-urlencoded". Causa erro "Missing final '@domain'" no SES.
Workaround obrigatório:
Node: HTTP Request
- Method: POST
- URL: https://email.us-east-2.amazonaws.com
- Authentication: Predefined Credential → AWS (kqmbhAWS0tyL5fuI)
- Service: ses
- Region: us-east-2
- Send Body: ON
- Content Type: Raw
- rawContentType: application/x-www-form-urlencoded
- Body (expression):
={{ 'Action=SendEmail&Source=' + encodeURIComponent($json.email_from) + '&Destination.ToAddresses.member.1=' + encodeURIComponent($json.email_to) + '&Message.Subject.Data=' + encodeURIComponent($json.email_subject) + '&Message.Body.Text.Data=' + encodeURIComponent($json.email_body) }}
NUNCA usar:
- Node nativo n8n-nodes-base.awsSes (mesmo bug)
- contentType: "form-urlencoded" com AWS auth
- SMTP node (não configurado, SES API é mais confiável)
Chatwoot — Endpoints usados¶
Base URL: https://chat.bepex.com.br/api/v1/accounts/2
⚠️ Instância correta é chat.bepex.com.br, account_id 2 (confirmado via resposta real da API e execuções de produção em 2026-05-13). Inboxes de prospecção (62/63/64) existem nesta instância, NÃO em atendimento.agencialacos.com.br.
| Operação | Método | Endpoint |
|---|---|---|
| Buscar contato | GET | /contacts/search?q={telefone} |
| Criar contato | POST | /contacts |
| Criar conversa | POST | /conversations |
| Enviar mensagem | POST | /conversations/{id}/messages |
| Adicionar labels | POST | /conversations/{id}/labels — body: {"labels":["ia-atendendo"]} |
Inboxes de prospecção:
| Inbox | ID | Slot | Número WhatsApp |
|---|---|---|---|
| Prospecção 1 | 62 | A | (número conectado via API bridge) |
| Prospecção 2 | 63 | B | (número conectado via API bridge) |
| Prospecção 3 | 64 | C | (número conectado via API bridge) |
ATENÇÃO: Account 3 também tem inbox 25 (pessoal Rafael) e inbox 26 (Whats Bepex comercial). Nunca confundir com as inboxes de prospecção.
Auth direta via Chrome (quando o MCP Chatwoot não cobre a conta): o MCP 46639869... só cobre atendimento.agencialacos.com.br (contas 3/4/5/7/8/9) — não alcança chat.bepex.com.br (account 2). Com o browser do Rafael já logado em chat.bepex.com.br, os headers de auth ficam expostos em window.axios.defaults.headers.common (via javascript_tool): access-token, client, uid, token-type, expiry. Usar esses 5 headers em fetch() direto pra qualquer endpoint /api/v1/accounts/2/... sem precisar reautenticar. Validado em 2026-07-03/04 pra auditoria de 538 conversas. Endpoint útil para filtro server-side por label: POST /api/v1/accounts/2/conversations/filter com body {payload:[{attribute_key:"labels", filter_operator:"equal_to", values:["<label>"], query_operator:null}]} (o param de query ?labels[]= não funciona).
PostgreSQL — Queries úteis¶
Ver leads ativos e próxima ação¶
SELECT id, nome, email, sender_slot, sequence_day, sequence_status, next_action_date
FROM representantes
WHERE sequence_status = 'active'
ORDER BY next_action_date, sender_slot;
Inserir lead manualmente¶
INSERT INTO representantes (nome, email, telefone, empresa, cidade, estado, linkedin_url, sender_slot, sequence_day, sequence_status, next_action_date, created_at, updated_at, notas)
VALUES (
'Nome do Lead',
'email@empresa.com',
'11999999999',
'Empresa X',
'São Paulo',
'SP',
NULL,
'A', -- ou B, ou C (round-robin manual)
0,
'active',
CURRENT_DATE,
NOW(),
NOW(),
'Fonte: ExpoRevestir 2026'
)
RETURNING id, nome, email, sender_slot;
Pausar/retomar lead¶
-- Pausar
UPDATE representantes SET sequence_status = 'paused' WHERE id = X;
-- Retomar
UPDATE representantes
SET sequence_status = 'active', next_action_date = CURRENT_DATE
WHERE id = X;
Opt-out (lead pediu pra não receber mais)¶
Via WhatsApp: manual — setar direto no banco:
UPDATE representantes
SET sequence_status = 'opted_out', updated_at = NOW()
WHERE id = X;
Via email: automático — o lead clica no link de descadastro no rodapé do email. O WF-Rep-Optout (IGBDGuIDLMpfCX8O) processa e atualiza o banco.
URL de descadastro: https://n8n-hook.bepex.com.br/webhook/rep-optout?email=EMAIL_CODIFICADO
O rodapé já está nos emails dos dias 0, 3 e 22 (Code node "Prepara Email (SES)" do Worker).
Catálogo Bepex (link)¶
- Dropbox (direct download):
https://www.dropbox.com/scl/fi/df3jjfzkg3noa5zy9zkbj/Catalogo-Bepex-_.pdf?rlkey=qw1tbys8ntmeuw13bqo05gpl8&dl=1 - Usado nas copies dos dias 21 (WhatsApp) e 22 (email)
- ⚠️ O parâmetro
st=do link original foi removido para estabilidade. Se o link deixar de funcionar, gerar novo shared link no Dropbox e atualizar os dois nós (Prepara WhatsApp (Chatwoot)ePrepara Email (SES)) no WF-Rep-Worker.
Buscar por nome para opt-out manual:
SELECT id, nome, email, telefone, sequence_status
FROM representantes
WHERE nome ILIKE '%nome_do_lead%';
Ver leads em recontato (pós-incidente inbox 63):
SELECT id, nome, sender_slot, sequence_day, sequence_status, next_action_date
FROM representantes
WHERE recontato_inbox63 = TRUE
ORDER BY next_action_date;
recontato_inbox63 = TRUEidentifica os 52 leads do Grupo B (DataAdv V2) que perderam WhatsApp durante a desconexão da inbox 63 (09/jun–24/jun/2026) e foram resetados para day 7 em 2026-06-30. Quando esses leads chegam ao dia 22 (email catálogo), o "Recontato Email Guard" no Worker os marcacompletedsem enviar o email. A flag é zerada (FALSE) ao marcar completed.
Estatísticas da base¶
SELECT
sequence_status,
COUNT(*) as total,
COUNT(CASE WHEN email IS NOT NULL THEN 1 END) as com_email,
COUNT(CASE WHEN telefone IS NOT NULL THEN 1 END) as com_telefone
FROM representantes
GROUP BY sequence_status;
Caixas de email (Hostinger)¶
| Slot | Senha | Encaminha para | |
|---|---|---|---|
| andrei.mazer@bepexmkt.com.br | A | ${BEPEXMKT_EMAIL_PASSWORD} |
andrei@bepexmkt.com.br |
| andrei@bepexmkt.com.br | B | ${BEPEXMKT_EMAIL_PASSWORD} |
— (caixa principal) |
| a.mazer@bepexmkt.com.br | C | ${BEPEXMKT_EMAIL_PASSWORD} |
andrei@bepexmkt.com.br |
Webmail: https://mail.hostinger.com (ou via hPanel > Emails) Domínio: bepexmkt.com.br (Hostinger, plano Business Email gratuito)
Calendly¶
- Link público: https://calendly.com/rafael-bepex/30min
- Conta: rafael@bepex.com.br
- Calendar conectado: Google Calendar "Agenda Representantes" (mesma conta)
- Configurado em: 2026-04-17
Fonte de leads¶
ExpoRevestir 2026 (lote atual)¶
- Arquivo:
ExpoRevestir_2026_Representantes_MASTER_DEDUP.csv - Total: 460 registros
- Com email + telefone: 12 (10 inseridos como lote teste, 2 duplicados)
- Com email apenas: 5
- Com telefone apenas: 4
- Só LinkedIn: 162
- Sem contato: 277
Apollo.io (fonte original do protótipo)¶
- Filtros definidos no protótipo para 200 contatos
- Não usado no lote atual (ExpoRevestir foi priorizado)
Filtros Pipedrive — Reengajamento de reps já cadastrados (2026-07-03)¶
Frente paralela: usa o Pipedrive (não o banco representantes) para achar representantes antigos e reengajar. Filtros salvos na conta do Pipedrive Bepex (dono = usuário Enrico, visible_to=1; API não altera visibilidade — só UI).
| Filtro | ID | Tipo | Contagem (2026-07-03) | Link |
|---|---|---|---|---|
| REPS 01 — Pessoas ligadas a Representação | 139534 |
people | 167 pessoas | https://bepex.pipedrive.com/persons/list/filter/139534 |
| REPS 02 — Negócios de Representação | 139535 |
deals | 237 negócios | https://bepex.pipedrive.com/deals/list/filter/139535 |
Lógica (grupo OR — operator: LIKE '%$%'):
- 139534 (Pessoas) — organização.Nome ~
representa/agente comercial/agenciamento comercial; organização.CNAE(nome) ~representa; pessoa.Nome ~representa; negócio.Título ~representa/agente comercial/agenciamento comercial. - 139535 (Negócios) — negócio.Título ~
representa/agente comercial/agenciamento comercial; organização.Nome ~representa/agenciamento comercial; organização.CNAE(nome) ~representa.
Field IDs numéricos do filtro (não confundir com field keys hash):
| Objeto | Campo | field_id | key/obs |
|---|---|---|---|
| organization | Nome | 3993 |
— |
| organization | CNAE (nome) Date Solutions | 4029 |
key 7260d417366db01613178e83765748753fcfaf7e |
| organization | CNAE (número) Date Solutions | 4028 |
key e9bd20c...; 46.1x = representantes comerciais |
| person | Nome | 9038 |
— |
| deal | Título | 12451 |
— |
| deal | Organização | 12457 |
— |
Estrutura de conditions (API): {glue:"and", conditions:[ {glue:"and", conditions:[]}, {glue:"or", conditions:[<cada condição>]} ]} — grupo AND vazio + todas no grupo OR. Cada condição: {object, field_id:"<num>", operator:"LIKE '%$%'", value:"<texto>", extra_value:null, json_value_flag:false}. Criar/editar filtro exige api_token (POST/PUT /api/v1/filters); o token pessoal fica em /settings/api (usar só em memória, nunca logar). Sessão de browser NÃO cria filtro (writes 401), só lê.
Entregável do levantamento: Representantes_Pipedrive_Levantamento.html (no Drive — ver ARQUIVOS.md).
Prova de cobertura: 53 orgs com CNAE 46.1x → 100% já capturadas pelos filtros. Distribuidoras (~344) e revendas (~13) ficam FORA (categoria distinta; filtros próprios "Distr Ferro e Aço", "DISTRIBUIDORES").
Camada de IA para WhatsApp (2026-07-07)¶
Varia o texto de cada WhatsApp (dias 7/14/21) com IA leve + cap de volume por número. Email intocado. Worker
KrMlipmgxuc2EdlL= 40 nós, deixado INATIVO.
LLM¶
| Item | Valor |
|---|---|
| Endpoint | https://openrouter.ai/api/v1/chat/completions (HTTP Request, predefinedCredentialType → openRouterApi) |
| Credencial | OpenRouter Diversos Bepex — b11busTUZQ9ihsS9 |
| Modelo | google/gemini-2.5-flash (o slug gemini-2.0-flash-001 dá 404 nessa conta) |
| Params | temperature 0.95, top_p 0.95, max_tokens 500, neverError:true |
| Custo | ~US$ 0,0003–0,0006 por mensagem |
Nós novos no WF-Rep-Worker¶
Conta WA Hoje (pg) → Atingiu Cap? (IF, enviados_hoje >= 20) → Adia Lead +1 (pg) / Responde Cap · Busca Anti-Repeticao (pg) → Monta Prompt WA (code) → IA Reescreve WA (http) → Valida e Fallback WA (code) → (Chatwoot) → Grava Historico WA (pg).
Prepara WhatsApp (Chatwoot) agora lê $('Roteia por Canal e Monta Payload').first().json (não $input).
Cap diário¶
- 20 por inbox/dia. Editar no nó IF "Atingiu Cap?" (
rightValue). Ao bater,Adia Lead +1faznext_action_date = CURRENT_DATE + 1(não avançasequence_day, não envia).
Tabela rep_msg_historico¶
CREATE TABLE rep_msg_historico (
id SERIAL PRIMARY KEY,
rep_id INTEGER,
canal VARCHAR(20) NOT NULL, -- 'whatsapp'
effective_sequence_day INTEGER, -- 7/14/21
inbox_id INTEGER, -- 62/63 (cap por número)
message TEXT NOT NULL,
opening TEXT, -- 1ª linha, para a avoid-list
used_ai BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_rmh_dedup ON rep_msg_historico(canal, effective_sequence_day, created_at DESC);
CREATE INDEX idx_rmh_cap ON rep_msg_historico(canal, inbox_id, created_at);
SELECT opening, message ... WHERE canal='whatsapp' AND effective_sequence_day = X ORDER BY created_at DESC LIMIT 10 → vira "não repita" no prompt.
- Cap: SELECT COUNT(*) ... WHERE canal='whatsapp' AND inbox_id=X AND created_at::date = CURRENT_DATE.
Diretrizes de copy embutidas no prompt (Monta Prompt WA)¶
Citar Bepex + fachadas metálicas sob medida (validação exige "bepex"+"fachada"); SEM território; SEM nome próprio (rejeita "andrei"/"meu nome"); citar Hunter Douglas/Sulmetais (qualidade equivalente, preço competitivo); enquadrar como produto de ticket alto pro portfólio.
Artefatos¶
| O que | ID |
|---|---|
| Backup pré-IA (server-side) | 8OYF0qGGSBYxWCey |
| Workflow de teste de amostras (sem envio) | HwNoelEpdkNVBglJ |
Reativação (quando o Rafael der o go)¶
- Confirmar por que o Worker estava inativo. 2. GET → PATCH (se precisar) → GET versionId fresh →
deactivate→activate. 3. Validar 1 lead real E2E abrindo o output nó-a-nó (trust content, not status).
Prospecção via LinkedIn Sales Navigator (2026-07-07)¶
Frente de descoberta de novos reps (só levantamento de nomes; contato enriquecido depois no DataAdv). Achado/decisão/status: ver
historico.md2026-07-07. Spec operacional turnkey: o spec turnkey de Sales Navigator (verARQUIVOS.md).
Queries booleanas (campo de busca; filtro Localidade = estado)¶
Cargos (grupo base, reutilizado em tudo):
"representante comercial" OR "representante de vendas" OR "representante técnico" OR "agente comercial" OR "representação comercial"
Recorte relevante (~7,5k Brasil): <cargos> AND (fachada OR fachadas OR "fachada ventilada" OR "pele de vidro" OR brise OR ACM OR "metal expandido" OR "metal perfurado" OR "chapa perfurada" OR "revestimento metálico" OR esquadria OR esquadrias OR revestimento OR revestimentos OR porcelanato OR cerâmica OR "revestimento cerâmico")
SP bloco A (~1k): <cargos> AND (fachada OR fachadas OR "fachada ventilada" OR "pele de vidro" OR brise OR ACM OR "metal expandido" OR "metal perfurado" OR "chapa perfurada" OR "revestimento metálico" OR esquadria OR esquadrias)
SP bloco B (~2k): <cargos> AND (revestimento OR revestimentos OR porcelanato OR cerâmica OR "revestimento cerâmico")
Filtro externo/autônomo (~2k, corta o relevante): <relevante> AND ("representações" OR autônomo OR represento OR representamos OR multimarcas OR "escritório de representação" OR "representante externo")
Volume medido (Brasil, contador do Sales Nav, 2026-07-07)¶
| Recorte | Volume |
|---|---|
| Cargo só | ~470 mil |
| Envelope amplo (+vidro/alumínio/vidraçaria/serralheria) | ~10 mil |
| Relevante | ~7,5 mil |
| Externo/autônomo | ~2 mil |
| Núcleo (fachada/metal/ACM/pele de vidro) | ~709 |
Fatiamento (teto de 2.500 por busca)¶
Regra: 1 UF = 1 fatia; se a fatia ler ≥ 2.500, split A/B por bloco de setor. Só SP estoura (medido).
| Fatia | Contagem | geoID (filtro REGION) |
|---|---|---|
| Brasil (referência) | 470 mil | 106057199 |
| São Paulo (estado) | 3.000 → split A+B | 105871508 |
| ↳ SP-A / SP-B | ~1k / ~2k | (mesmo geoID SP) |
| Minas Gerais (2º maior) | 673 | 100358611 |
| Demais 25 estados | < 673 | obter no filtro Localidade → "UF, Brasil" |
Total: 28 buscas. Split de SP bate exato (1k+2k=3k, sobreposição ~0; dedup por URL). Sub-split por bloco de setor (não cidade) porque é exaustivo. Ordem por volume: SP-A → SP-B → MG → RS → PR → SC → RJ → ES → BA → GO → DF → … → Norte/Nordeste menores.
Classificação por regex (sem IA no dado)¶
- externo:
/representa(ç|c)(õ|o)es|representa(ç|c)ão comercial|aut[oô]nomo|represento|representamos|multimarcas|escrit[óo]rio de representa|representante externo/iem (empresa+bio) - interno_provavel: empresa casa fabricante
/eliane|portobello|portinari|deca|roca|incepa|biancogres|damme|gaudi|elizabeth|ceusa|itagres|viapol|sika|weber|quartzolit|carmelo fior/i - resto: indefinido (revisar)
Quirks técnicos (Chrome MCP) — validados¶
- Lista virtualizada: só renderiza com scroll físico (
computer), na aba em foco.scrollTopvia JS NÃO renderiza (testado: trava). Aba sem foco = 0 cards → não paraleliza em abas. windowsome ao navegar → master emlocalStorage. Resultado de tool trunca ~1024 chars → exportar via download de Blob prauma pasta de Downloads local, não pelo canal de resultado.- Boolean (AND/OR/aspas/parênteses) funciona no campo.
AND NOT (a OR b)retorna 0 (quebra) — limpar ruído apertando termos. - Filtro de região persiste ao trocar a query. "São Paulo e Região" (metro) ≠ "São Paulo, Brasil" (estado) — sempre o estado. API interna de busca = GraphQL ofuscado (
/7O4iJ3weJ...), não usar. - Extração NÃO gasta crédito de desbloqueio (bio visível na lista); os ~25 créditos/mês só revelam contato.