Clínica Lindomar Delgado · Guia operacional

Integração SymplesDesk ↔ Pipedrive ↔ Clínica nas Nuvens

Serviço que liga o atendimento de WhatsApp (SymplesDesk) ao CRM (Pipedrive) e, desde 26/08, o CRM ao prontuário (Clínica nas Nuvens) — gravando tudo em um banco de dados próprio, em tempo real. Esta página explica o que cada parte faz e o que é preciso configurar para cada uma funcionar — sem precisar abrir o repositório.

Atualizado em 17/09/2026 · versão em produção, validada ponta a ponta

Visão geral

A configuração de cada médico (funil, etapa, responsável, canal de WhatsApp) mora em três tabelas do banco (integration_channels, integration_pipelines e integration_pipeline_steps) — trocar de ambiente de teste para produção é editar essas linhas e o .env, nunca o código.

Além dos 4 fluxos, o serviço mantém o quadro de Oportunidades do SymplesDesk espelhando o CRM nos dois sentidos — hoje em dois funis: o de Leads, das SDRs, e o de Consulta, das Concierges — e encerra a conversa 24h depois de um lead ser dado como perdido. Essas partes não são "Fluxo 5 e 6": nasceram depois e funcionam em paralelo aos quatro.

Desde 26/08 o serviço também cuida do prontuário no Clínica nas Nuvens quando uma Consulta é ganha: procura o paciente lá, liga ao contato se já existir e cria o cadastro se não existir. Detalhe na seção Cadastro no Clínica nas Nuvens.

Conceito central: pessoa ≠ ciclo

A regra mais importante de todo o sistema:

Entrada de lead (SDR)

gatilho: webhook "Novo atendimento" (NewTicket) do SymplesDesk

Quando um paciente inicia conversa no WhatsApp, o SymplesDesk abre um ticket e avisa o serviço, que cria o lead no funil certo do médico certo — sem duplicar paciente nem ciclo.

Como funciona

  1. Confere se esse ticket já foi processado (reenvio do SymplesDesk não duplica nada).
  2. Identifica o médico pelo canal de WhatsApp que recebeu a mensagem (config no banco).
  3. Normaliza o telefone e acha ou cria o contato no banco.
  4. Se o contato já tem ciclo ativo com esse médico → não cria nada (evita deal duplicado).
  5. Se não tem: busca a pessoa no Pipedrive pelo telefone, em qualquer formato (+55, hífen, 9º dígito), varrendo até 300 candidatos. Quem já tem cadastro (com DDD válido) é vinculado — e o nome do cadastro preenche o banco. Cadastro sem DDD é ignorado de propósito (está errado no CRM): cria-se pessoa nova no formato correto 55+DDD+número.
  6. Cria o deal no funil de Leads do médico — título telefone - sigla, responsável, etapa e etiqueta vindos da config.
  7. Grava tudo no banco: ciclo ativo, deal, conversa e trilha de auditoria.

O que precisa para funcionar

  • Criar o webhook no SymplesDesk (um por canal), com o evento de novo atendimento marcado, e adicionar a URL da API: <URL-do-serviço>/webhooks/symplesdesk. Importante: esse evento deve apontar para uma URL só — dois destinos criariam deal duplicado.
  • Preencher a tabela integration_channels no banco (canal → médico → funil/etapa/responsável) — o SQL pronto está em Como adicionar um novo canal.
  • Definir no ambiente: PIPEDRIVE_BASE_URL e PIPEDRIVE_API_TOKEN.

Deal perdido / ganho

gatilho: webhook do Pipedrive (deal.updated, mudança de status)

Quando a equipe marca um deal como perdido ou ganho no Pipedrive, o serviço reage:

Como funciona

  1. Perdido: o serviço deixa uma nota interna no atendimento com o motivo —
    Deal perdido no Pipedrive: [título] / Motivo: [motivo da perda] — e o ciclo vira closed_lost no banco. A conversa NÃO fecha neste momento: ela fecha 24 horas depois, se o paciente não voltar (ver Fechamento da conversa).
  2. Ganho: atualiza o banco (closed_won) e a conversa até o momento do ganho entra no deal na hora (ver Fluxo 4). Não fecha a conversa — o atendimento pós-ganho continua no WhatsApp (decisão da clínica).
  3. Deal perdido que o banco ainda não conhece (criado à mão, ou anterior à integração) não é ignorado: o serviço busca a pessoa no Pipedrive, resolve o contato pelo telefone e fecha mesmo assim.
  4. Se a API do SymplesDesk falhar, o banco é atualizado mesmo assim e um alerta no Discord avisa a equipe (a conversa não fica esquecida aberta sem ninguém saber).
  5. O card da SDR também fecha, com o mesmo resultado do deal — ganho ou perdido (ver Cards de Oportunidade).
Mudou em 07/08/2026. Até então a conversa fechava na hora da perda, por uma configuração da tela do SymplesDesk (não pelo código). Isso encurtava a janela de recuperação do paciente. As duas configurações foram trocadas para "Manter aberto", e o fechamento passou a ser o de 24h.

O que precisa para funcionar

  • Criar um webhook no Pipedrive (Configurações → Ferramentas → Webhooks, versão 2): objeto deal, ações create + change, e adicionar a URL da API: <URL-do-serviço>/webhooks/pipedrive, preenchendo usuário e senha (Basic Auth) com os mesmos valores do ambiente.
  • Criar um 2º webhook no Pipedrive para o sync de nome do contato: objeto person, ação change, mesma URL e mesma Basic Auth. Sempre que alguém editar o nome de uma pessoa no Pipedrive, o serviço atualiza o nome no banco (só o nome — o telefone é a identidade do contato e nunca muda por aqui).
  • Definir no ambiente os tokens da API externa do SymplesDesk, um por canal (SYMPLESDESK_TOKEN_T e SYMPLESDESK_TOKEN_A) — já validados contra a produção em 22/07/2026.
  • Opcional: DISCORD_ERROR_WEBHOOK_URL para os alertas de falha.

Captura Concierge e movimentação

gatilho: o mesmo webhook do Pipedrive (deal.added e deal.updated)

A automação só cria deal no funil de Leads (Fluxo 1). Nos funis de Consulta e Procedimentos, os deals são criados à mão pela equipe Concierge — este fluxo os captura para o banco, rastreando a jornada completa do paciente.

Como funciona

  1. Deal novo criado à mão (Consulta/Procedimentos): o serviço resolve o paciente pelo telefone da pessoa do Pipedrive e grava o deal no banco. Nunca captura no funil de Leads (esse é papel do Fluxo 1) e nunca cria nada no Pipedrive — só observa.
  2. Qualquer movimentação de deal (troca de etapa, troca de funil, reabertura): o banco sobrescreve a etapa atual do deal — ele espelha sempre o estado de agora, sem histórico de etapas.
  3. Deal desconhecido que se move: é capturado na hora, com a etapa atual. Se for um deal aberto no funil de Leads, também ganha um ciclo ativo no banco.
  4. Deal de funil que não é da integração (qualquer outro funil da conta) é ignorado.

O que precisa para funcionar

  • O mesmo webhook do Pipedrive do Fluxo 2 — um único webhook cobre os dois fluxos, nada extra pra criar.
  • Preencher a tabela integration_pipelines com os funis mapeados (Leads, Consulta e Procedimentos por médico) — o SQL pronto está em Como adicionar um novo canal.

Conversas do WhatsApp dentro do deal

gatilho: webhook "Mensagem criada" + job diário 19:00 + fechamento do deal

Todas as mensagens trocadas no WhatsApp aparecem dentro do deal do paciente no Pipedrive, como uma atividade por dia de conversa. São duas partes:

Parte A — cada mensagem entra no banco

  1. O SymplesDesk envia um webhook a cada mensagem (recebida ou enviada), autenticado por token.
  2. O serviço grava a mensagem em symplesdesk_messages, vinculada ao contato certo (pelo ticket ou pelo telefone). Mensagens repetidas (reenvio) e apagadas não duplicam nem entram.
  3. Segurança: o bloco de configuração que o fornecedor ecoa no payload (contém tokens) é removido antes de gravar.

Parte B — a conversa vira Activity no deal (dois gatilhos)

  1. Todo dia às 19:00 (Brasília) — horário ajustável pela variável CONVERSATION_SYNC_CRON, sem mexer em código: o job agrupa as mensagens pendentes por paciente e por dia (incluindo o próprio dia até o horário do disparo) e cria uma Activity "Whatsapp - DD/MM/AAAA" por dia de conversa — somente em deals ABERTOS dos funis de Leads e Consulta. Paciente sem deal aberto: as mensagens aguardam (nada se perde) e entram no próximo deal.
  2. Na hora do fechamento: quando um deal de Leads/Consulta é marcado como ganho ou perdido, toda a conversa pendente até aquele instante entra imediatamente nesse deal — sem esperar as 19h. (Ganho na SDR = a conversa completa até o ganho já fica no deal.)
  3. Cada mensagem sincronizada guarda no banco qual deal a recebeu e quando — uma mensagem nunca vai pra dois deals nem é enviada duas vezes.
  4. Se o serviço ficou fora do ar no horário, a próxima execução recupera os dias perdidos sozinha. Dias gigantes são divididos em partes ("Parte 1/2, 2/2…"). Falha em um paciente não derruba os demais e dispara alerta no Discord.

Formato da nota (mensagens do mesmo minuto são agrupadas, com o horário uma vez no fim; atendente aparece pelo nome):

Oi, tudo bem? Queria marcar.
15:07

Gabriel:
Oi! Claro, vou te passar as opções.
15:07

O que precisa para funcionar

  • Criar o webhook "Mensagem criada" nas 2 configurações de API do SymplesDesk (uma por canal) e adicionar a URL da API: <URL-do-serviço>/webhooks/symplesdesk-newmessage, colando no campo "Token de autenticação" o mesmo valor de SYMPLESDESK_WEBHOOK_TOKEN do ambiente.
  • Marcar somente o evento "Mensagem criada" na tela (outros eventos são ignorados com segurança, mas geram tráfego à toa).
  • Definir no ambiente: PIPEDRIVE_ACTIVITY_TYPE_WHATSAPP_KEY — a chave do tipo de atividade "Whatsapp" na conta do Pipedrive (sem ela o job falha de propósito, para nunca criar atividade de tipo errado).

Cards de Oportunidade — o quadro do SymplesDesk

gatilho: mão dupla — deal muda no Pipedrive, ou card muda no quadro

As SDRs trabalham no quadro de Oportunidades do SymplesDesk, sem precisar abrir o Pipedrive. Cada deal de Leads tem um card espelho, e os dois lados se acompanham nos dois sentidos: mover o card move o deal, e mover o deal move o card.

Do card para o deal

A SDR faz no cardAcontece no Pipedrive
Arrasta para outra etapao deal muda de etapa no funil
Marca como ganhoo deal vira ganho
Marca como perdidoo deal vira perdido, com o motivo
Renomeia o cardo nome da pessoa é corrigido no CRM

Do deal para o card

Alguém faz no PipedriveAcontece no quadro
Move o deal de etapao card acompanha
Ganha ou perde o dealo card fecha com o mesmo resultado
Corrige o nome da pessoao card é renomeado
Reabre um deal fechadoo card volta a abrir

Quando o card aparece

  1. Lead novo: o card nasce junto com o deal, na etapa de entrada.
  2. Paciente antigo que volta a falar: o card é criado na hora da mensagem, já na etapa em que o deal está hoje — nunca na etapa de entrada, para o quadro não mentir sobre o estado do lead.
  3. Paciente que nunca voltou a falar fica sem card de propósito: sem conversa não há o que trabalhar no quadro.
Duas regras que evitam confusão. (1) O telefone nunca sobrescreve um nome. Quando o paciente ainda não tem cadastro, o card nasce com o telefone — mas assim que alguém escreve o nome de verdade, o telefone nunca mais volta por cima. (2) Eco não vira pingue-pongue. Toda mudança que chega é comparada com o estado atual: se já está igual, o sistema para. Foi o que impediu 326 idas e voltas desnecessárias em 30 horas.

O que precisa para funcionar

  • Criar o webhook "Oportunidade" nas configurações de API do SymplesDesk, apontando para <URL-do-serviço>/webhooks/oportunidade e colando no campo "Token de autenticação" o valor de SYMPLESDESK_OPORTUNIDADE_WEBHOOK_SECRET.
  • Preencher o mapa de etapas (integration_pipeline_steps) — ver a seção Mapa das etapas. Sem a tradução, o movimento é ignorado com registro: o sistema nunca chuta uma etapa.
  • Definir a SDR dona dos cards de cada canal (symplesdesk_responsible_user_id): é por esse campo que cada uma filtra o quadro compartilhado para ver só os cards dela.
  • Definir o motivo de perda fixo de cada canal (symplesdesk_lost_reason_id): o fornecedor recusa fechar card como perdido sem motivo. O motivo real continua no Pipedrive e na nota — este é só o exigido pela tela.

Cards de Consulta — o quadro da Concierge

gatilho: negócio de Consulta criado no ganho do lead, ou paciente com consulta aberta que volta a falar

Quando um lead é ganho, o Pipedrive cria sozinho o negócio no funil de Consulta — isso já acontecia e continua igual. A novidade é que esse negócio agora também ganha um card, no funil "Funil de Consulta - Concierge" do quadro, com a Concierge do médico como dona. A Concierge passa a trabalhar no mesmo lugar que a SDR, sem abrir o Pipedrive.

Quando o card aparece

  1. No ganho do lead: assim que o Pipedrive cria o negócio de Consulta, o card nasce — em poucos segundos, na primeira etapa do funil.
  2. Paciente que já tinha consulta aberta: o card nasce na hora em que ele manda mensagem, já na etapa em que o negócio está hoje. É assim que a fila antiga entra no quadro: aos poucos, conforme cada paciente volta a falar — sem despejar centenas de cards de uma vez.

O que muda em relação ao card de Leads

 Card de Leads (SDR)Card de Consulta (Concierge)
Dona do carda SDR do médicoa Concierge do médico
Nome do cardmontado pelo sistema: nome - siglacópia exata do nome do negócio: CONSULTA DE nome
Etapasas do funil de Leadsas do funil de Consulta
Renomear o cardcorrige o nome da pessoa no CRMnão propaga nada — ver abaixo

O que funciona igual

Mover o card de etapa move o negócio no funil de Consulta, e vice-versa. Ganhar ou perder o card fecha o negócio com o mesmo resultado, e reabrir também acompanha. É a mesma mão dupla dos cards de Leads — só o renomear ficou de fora.

Por que renomear não propaga. O card de Consulta copia o nome do negócio letra por letra — é assim que os dois lados ficam reconhecíveis como a mesma coisa. Se o sistema também tentasse "corrigir" nomes por aqui, ele empurraria o texto inteiro ("CONSULTA DE Maria") por cima do nome da paciente no CRM. Já aconteceu uma vez, em 06/08, e foi por isso que a regra virou: nome de card de Consulta nasce colado ao do negócio e ninguém mexe depois.
O nome do card é uma fotografia do momento do ganho. Se a paciente ainda estava cadastrada com o telefone quando o lead foi ganho, o card de Consulta nasce com o telefone — e não muda sozinho depois, mesmo que alguém corrija o cadastro. Para renomear, é na tela, dos dois lados.

O que precisa para funcionar

  • Definir a Concierge dona dos cards de cada canal (symplesdesk_concierge_user_id): é o campo que liga esta parte. Vazio, nenhum card de Consulta é criado — e o resto do sistema segue normal.
  • Preencher o mapa de etapas do funil de Consulta (integration_pipeline_steps com pipeline_type = 'consulta') — ver a seção Mapa das etapas.
  • Nada de webhook novo: a captura usa os mesmos que os fluxos 3 e 4 já usam.
A automação do Pipedrive continua sendo a dona do negócio. Quem cria o negócio de Consulta no ganho é ela, como sempre foi — o serviço só acompanha, criando o card e ligando os dois. Se um dia a automação criar dois negócios iguais, aparecerão dois cards: o quadro espelha o CRM, não o corrige.

Fechamento da conversa, 24h depois da perda

gatilho: verificação automática de hora em hora, aos :15

Quando um lead é dado como perdido, a conversa dele no WhatsApp não fecha na hora: fecha 24 horas depois. A janela existe para o paciente ter tempo de responder — muita reversão acontece nesse intervalo.

Ao fechar, o sistema deixa uma nota interna no atendimento: "Conversa encerrada automaticamente — lead perdido há 24h." O paciente não recebe nada.

Duas travas antes de fechar

  1. Se o paciente voltou — tem qualquer negócio aberto — a conversa segue viva. Perdeu um lead mas abriu outro é paciente ativo, não caso encerrado.
  2. Se existe conversa mais recente que a do lead perdido, o sistema não fecha: fechar a errada seria pior que não fechar.
Por que existe um teto por rodada. O sistema fecha no máximo um punhado de conversas por execução. Não é limitação técnica: é proteção do fornecedor, cuja API já desativou um webhook nosso sozinha depois de receber falhas em sequência. Com o teto, o que sobra é fechado na hora seguinte — nada se perde.

Quando a SDR fecha na mão

Se alguém encerra o atendimento manualmente no SymplesDesk, o sistema fica sabendo e registra — e por isso não tenta fechar de novo o que já está fechado.

O que precisa para funcionar

  • Marcar o evento "Atendimento finalizado" na configuração de webhook do SymplesDesk, apontando para <URL-do-serviço>/webhooks/atendimento-finalizado, com o valor de SYMPLESDESK_ATENDIMENTO_WEBHOOK_SECRET no campo "Token de autenticação".
  • ⚠️ A rota precisa estar no ar ANTES de criar o webhook: o fornecedor desativa sozinho uma configuração que aponta para endereço inexistente.
  • Opcional: CLOSE_LOST_CRON (horário) e CLOSE_LOST_MAX_PER_RUN (teto) — ver Ligar e desligar.

Cadastro no Clínica nas Nuvens

Quando uma Consulta é ganha — ou seja, o paciente passou de "Aguardando pagamento" e o negócio foi ganho no Pipedrive — o serviço procura essa pessoa no Clínica nas Nuvens. Já é paciente? Só registra a ligação. Não é? Cria o prontuário na hora, com os dados que já estão no Pipedrive.

Procurar antes de criar, e por quê

O Clínica nas Nuvens tem 42.126 pacientes e é a base da clínica há anos. Medimos a população exata deste gatilho: de cada 10 pessoas que ganham uma Consulta, 9 já são pacientes de lá. Criar sem procurar duplicaria prontuário em 9 de cada 10 vezes — e prontuário duplicado quebra histórico clínico, agenda e faturamento.

Por isso a busca vem primeiro, e ela é dupla: por CPF e, se não achar, por nome. A segunda existe porque só 20% dos prontuários do CnN têm CPF preenchido — "não achei pelo CPF" quase nunca significa "não existe". Na medição, sem a busca por nome o sistema teria criado 7 pacientes que já existiam, todos com prontuário antigo e sem CPF.

Quando as duas buscas não acham ninguém, o paciente é criado. Sem espera e sem fila.

Como funciona

  1. O deal de Consulta é marcado como ganho no Pipedrive.
  2. O serviço lê a pessoa no Pipedrive — CPF, data de nascimento e endereço.
  3. Procura no Clínica nas Nuvens pelo CPF. Achou exatamente um? Liga os dois e acabou.
  4. Não achou pelo CPF? Procura pelo nome. Se vier um único paciente e a data de nascimento bater, liga também.
  5. O nome também não achou ninguém? Cria o paciente no Clínica nas Nuvens e já registra a ligação.
  6. Achou dois ou mais parecidos, ou um só com nascimento diferente? Aí vai para a fila de revisãoninguém escolhe entre prontuários parecidos sozinho.
A data de nascimento é o que separa mãe e filha. Telefone repetido entre parentes é caso comum numa clínica, então o telefone não decide sozinho — ele no máximo confirma. Medido: o telefone não resolveu um caso sequer que o nascimento já não resolvesse.

O que isso entrega hoje

Rodado contra a base real, sobre 70 Consultas ganhas:

ResultadoQuantosO que significa
Ligados automaticamente61já eram pacientes; a ligação ficou registrada
Prontuários criados0ninguém precisou ser criado nessa carteira — todos os 70 já existiam no CnN. Daqui para a frente, quem for novo é criado
Foram para revisão9parecidos demais para decidir sozinho

A fila de revisão

A fila não é onde o paciente novo espera — esse é criado direto. Ela guarda só o que o sistema não pode decidir sozinho, na tabela cnn_match_reviews, com o motivo de cada caso, e serve de relatório para a clínica:

MotivoO que houve
ambiguodois ou mais pacientes parecidos — precisa de gente para escolher
sem_cpfa pessoa não tem CPF válido no Pipedrive, então nem dá para procurar
sem_nascimentofalta a data de nascimento — é obrigatória para criar no CnN
sem_nomeo "nome" no Pipedrive é o próprio telefone; viraria um prontuário chamado "5532991997896"
sem_pessoao contato não tem pessoa no Pipedrive
Um ganho de Consulta nunca é prejudicado por isto. Se o Clínica nas Nuvens estiver fora do ar, se o CPF estiver errado, se a criação falhar — o deal é ganho normalmente e o erro fica registrado. Esta parte nunca derruba o fluxo principal. E o mesmo paciente nunca é criado duas vezes, mesmo que o ganho seja registrado de novo: a ligação já gravada barra.

Agenda do dia seguinte no WhatsApp

gatilho: cron diário às 18h (America/Sao_Paulo)

Todo dia às 18h, o serviço lê a agenda de amanhã no Clínica nas Nuvens e manda, por WhatsApp, a lista para o grupo da clínica e, para cada médico configurado, a mesma lista no WhatsApp delesem passar pelo SymplesDesk nem pela Meta, e sem que a paciente veja nada.

Como funciona

  1. Lê no Clínica nas Nuvens os agendamentos do dia seguinte.
  2. Descarta quem não vai aparecer: agendamento cancelado ou marcado como faltou.
  3. Busca o nome de cada paciente no cadastro do Clínica nas Nuvens — é o nome que a recepção reconhece, não o nome do CRM (que às vezes é só o telefone).
  4. Busca o nome de cada profissional (médico ou fisioterapeuta) no catálogo de profissionais do Clínica nas Nuvens.
  5. Monta uma mensagem que abre com 📅 *Agenda de <dia da semana>, <dd/mm>* — <N> atendimentos no total e, logo abaixo, os atendimentos agrupados por profissional: o nome do profissional em negrito, com 🩺 na frente e quantos atendimentos ele tem no dia, e, embaixo dele, uma linha por atendimento no formato HH:MM Nome, por horário. Quem atende mais cedo aparece primeiro. Se o profissional de um atendimento não for encontrado, a linha sai assim mesmo, sob (profissional indisponível).
  6. Envia pela instância FZAP — um número de WhatsApp conectado por QR code, fora do SymplesDesk — ao destino de AGENDA_DESTINO: o grupo Agenda Pacientes Clínica Lindomar Delgado (ou um telefone).
  7. Para cada médico de AGENDA_MEDICOS, manda ao WhatsApp dele a mesma lista completa que foi para o grupo — tenha ele atendimento no dia ou não. A leitura da agenda é uma só para todos.
  8. Dia sem nenhum atendimento não gera mensagem alguma. E, depois de um envio que deu certo, a mesma data-alvo não sai de novo para aquele destino, mesmo que o job rode outra vez ou seja disparado à mão — cada destino (o grupo e cada médico) tem a sua própria trava.
Só profissional, horários e nomes. A mensagem não traz procedimento nem resumo de conversa — e não chega à paciente em nenhuma hipótese: o destino é sempre um número da equipe. Exemplo:
📅 *Agenda de quarta, 16/09* — 3 atendimentos no total

🩺 *Dr. Fulano de Tal* - 2 atendimentos
08:00 Maria Silva
11:00 Ana Souza

🩺 *Beltrana Fisioterapeuta* - 1 atendimento
08:15 Joana Lima
Os médicos de AGENDA_MEDICOS recebem exatamente esse mesmo texto. (no WhatsApp, o texto entre asteriscos aparece em negrito)
Se alguma coisa falha, o Discord avisa. Falha ao ler a agenda no Clínica nas Nuvens, número da FZAP desconectado (é preciso reconectar pelo QR code) ou envio que não vingou: os três casos geram alerta. Nos dois primeiros, nada é enviado. Uma lista acima de 4.000 caracteres sai em partes — se o envio falhar no meio, as partes que já saíram ficam no destino, e o alerta diz quantas. Se o envio para um médico falhar, o alerta diz qual médico, e o grupo e os outros médicos recebem normalmente.

O que precisa para funcionar

  • Definir no ambiente AGENDA_DESTINO (quem recebe a lista geral: o grupo 120363411914071233@g.us, ou um telefone só com dígitos e DDI) e TOKEN_FZAP (o token da instância) — o job só é agendado quando os dois existem (ver Ligar e desligar).
  • A instância FZAP precisa estar conectada (sessão ativa por QR code) na hora do disparo — desconectada, o serviço não envia e alerta pedindo a reconexão.
  • Opcional: AGENDA_CRON muda o horário do disparo (padrão 0 18 * * *, 18h) e FZAP_BASE_URL muda o endereço da instância, se um dia precisar trocar.
  • Opcional: AGENDA_MEDICOS manda a lista completa também ao WhatsApp de cada médico. Formato idpessoa:telefone, separados por vírgula — por exemplo 20689417:5532999999999. Par escrito errado é ignorado (e avisado no log), sem derrubar nada. Para incluir um médico: acrescentar o par e fazer redeploy. Os códigos:
    idpessoaProfissional
    20689417Dr. Thiago Ferreira Delgado
    20741719Dr. Augusto Cesar de Melo Almeida
    22509355Dr. Lindomar Delgado
    22647395Dra Larissa
    16918290Melissa Monica de Castro Teixeira
    27210466Riviane Marques da Costa Alves

Mapa das etapas — Pipedrive ↔ quadro do SymplesDesk administrador

Os dois sistemas nomeiam as etapas de formas diferentes e usam números próprios. A tabela integration_pipeline_steps é a tradução entre eles — e é ela que faz o card acompanhar o deal e vice-versa.

Funil de Leads (SDR)

EtapaNo quadro (SymplesDesk)Dr. ThiagoDr. Augusto
Cliente Potencial524637
Em conversa502736
Aguardando Pagamento483039

Funil de Consulta (Concierge)

EtapaNo quadro (SymplesDesk)Dr. ThiagoDr. Augusto
Consulta Agendada574755
Reagendamento584956
Consulta Confirmada594857

O quadro é um só para os dois médicos — por isso a mesma etapa (52, por exemplo) traduz para funis diferentes conforme o médico do deal. Quem separa o trabalho de cada SDR e de cada Concierge é o filtro por responsável, não o quadro.

Cuidado ao ler números soltos. Os dois sistemas numeram por conta própria, e os números se repetem entre funis: 48 é "Aguardando Pagamento" no quadro e "Consulta Confirmada" no funil de Consulta do Dr. Thiago — dois lugares completamente diferentes. Por isso a tradução leva sempre três informações: o médico, o funil e o número. Nunca cite um número sem dizer de que lado e de que funil ele é.
Etapa fora do mapa não quebra nada — e não passa despercebida. Se alguém criar uma etapa nova em qualquer um dos lados sem cadastrar a tradução, o movimento é ignorado com registro. O sistema prefere não fazer nada a chutar a etapa errada; mas o card para de acompanhar aquele deal em silêncio até alguém completar o mapa.

Ligar e desligar cada parte administrador

Toda automação pode ser desligada sem mexer em código e sem derrubar o resto. Útil para manutenção, teste, ou quando a clínica quer pausar um comportamento.

DesligarComoEfeito
Um médico inteiro integration_channels.is_active = false o canal some da configuração; nada daquele médico é processado
Criação de cards de Leads symplesdesk_responsible_user_id = NULL nenhum card novo de Leads é criado; os existentes seguem funcionando
Criação de cards de Consulta symplesdesk_concierge_user_id = NULL nenhum card novo de Consulta é criado; o funil de Leads não é afetado
Fechar card na perda symplesdesk_lost_reason_id = NULL o deal é perdido normalmente, o card fica aberto
Fechamento de 24h CLOSE_LOST_MAX_PER_RUN=0 a verificação roda e não fecha nada
Conversa diária no deal CONVERSATION_SYNC_CRON=0 0 30 2 * 30 de fevereiro nunca chega — o job nunca dispara
Criação de paciente no CnN CNN_CADASTRO_MODE=link continua procurando e ligando quem já é paciente, mas para de criar
Cadastro no CnN, por inteiro CNN_CADASTRO_MODE=off o ganho da Consulta segue normal; nada é procurado, ligado nem criado lá
Tarefas de NPS remover PIPEDRIVE_SURGERY_DATE_FIELD_KEY o job roda sem fazer nada
Agenda do dia seguinte deixar AGENDA_DESTINO ou TOKEN_FZAP vazio o job nem é agendado — nenhuma lista sai
Depois de mexer no banco, reinicie o serviço. A configuração dos canais é lida uma vez, na inicialização. Sem o restart, a mudança não vale — e o comportamento antigo continua, dando a impressão de que a chave não funcionou.
Teste a agenda antes de ligar de verdade. npx tsx scripts/agenda-dia-seguinte.ts mostra a mensagem que sairia hoje, sem mandar nada e sem gravar evento. Só envia de verdade com a flag --enviar.

O que a API precisa para funcionar administrador

Quem vai apenas integrar com a API não precisa desta seção — toda interação de fora é pelos endpoints e webhooks. Os blocos abaixo são de quem opera o serviço: variáveis de ambiente (segredos) e os webhooks apontados para a URL pública.

1 · Variáveis de ambiente (.env)

Os valores reais nunca ficam no código nem nesta página — vivem só no .env do servidor.

VariávelObrigatória?Para quê
DATABASE_URLSimConexão com o Postgres (fonte da verdade)
PIPEDRIVE_BASE_URLSimDomínio da conta Pipedrive (teste ou produção)
PIPEDRIVE_API_TOKENSimToken da API do Pipedrive
PIPEDRIVE_WEBHOOK_BASIC_USER / _PASSSimUsuário/senha que o webhook do Pipedrive usa para se autenticar
SYMPLESDESK_TOKEN_T / _AFluxo 2Tokens da API de fechar conversa, um por canal/médico
SYMPLESDESK_WEBHOOK_TOKENFluxo 4Token que autentica o webhook de mensagem (sem ele a rota recusa tudo)
PIPEDRIVE_ACTIVITY_TYPE_WHATSAPP_KEYFluxo 4Chave do tipo de atividade "Whatsapp" na conta
PIPEDRIVE_PROCEDURE_FIELD_KEYProduçãoCampo personalizado "Procedimento" do deal (não existe na conta de teste)
SYMPLESDESK_OPORTUNIDADE_WEBHOOK_SECRETCardsToken que autentica o webhook de Oportunidade (sem ele a rota recusa tudo)
SYMPLESDESK_ATENDIMENTO_WEBHOOK_SECRETFechamentoToken do webhook "Atendimento finalizado"
PIPEDRIVE_SURGERY_DATE_FIELD_KEYNPSCampo "Data da cirurgia"; ausente = tarefas de NPS desligadas
CONVERSATION_SYNC_CRONOpcionalHorário do job da conversa (cron, fuso Brasília; padrão 0 19 * * * = 19h)
CLOSE_LOST_CRONOpcionalQuando checar conversas a fechar (padrão 15 * * * * = de hora em hora, aos :15)
CLOSE_LOST_MAX_PER_RUNOpcionalTeto de fechamentos por rodada (padrão 25; 0 desliga)
NPS_SYNC_CRONOpcionalHorário do job de NPS (padrão 0 7 * * * = 7h)
CNN_CADASTRO_MODECadastro CnNNível do cadastro: off (padrão) · link (só reconhece) · full (reconhece e cria). Qualquer outro valor vira off — inclusive LINK em maiúscula
CNN_CLIENT_ID / CNN_CLIENT_SECRET / CNN_CIDCadastro CnNCredenciais da API do Clínica nas Nuvens. Faltando qualquer uma, todo ganho de Consulta vira evento de erro
PIPEDRIVE_CPF_FIELD_KEYCadastro CnNCampo "CPF" da pessoa. Sem ele, tudo cai em revisão por falta de CPF
PIPEDRIVE_BIRTH_DATE_FIELD_KEYCadastro CnNCampo "Data de Nascimento" da pessoa
PIPEDRIVE_ADDRESS_FIELD_KEYOpcionalCampo "Endereço" da pessoa (preenchido em ~5% da base; o cadastro funciona sem)
TOKEN_FZAPAgendaToken da instância FZAP (WhatsApp conectado por QR code) que envia a lista do dia seguinte
FZAP_BASE_URLOpcionalEndereço da instância FZAP; tem valor padrão de fábrica, só precisa ser definida se o endereço mudar
AGENDA_DESTINOAgendaQuem recebe a lista geral: o JID do grupo (…@g.us) ou um telefone (só dígitos, com DDI). Sem ele ou sem TOKEN_FZAP, o job nem é agendado
AGENDA_MEDICOSOpcionalMédicos que também recebem a lista completa no próprio WhatsApp: idpessoa:telefone separados por vírgula (ver a seção da agenda)
AGENDA_CRONOpcionalHorário do envio (cron, fuso Brasília; padrão 0 18 * * * = 18h)
DISCORD_ERROR_WEBHOOK_URLOpcionalAlertas operacionais de erro
PORTOpcionalPorta HTTP (padrão 3000)
Chave de campo personalizado errada derruba a leitura inteira. As chaves acima são hashes de 40 caracteres, e o Pipedrive responde HTTP 400 quando recebe uma que não existe — não é o campo que fica vazio, é a pessoa que não é lida. Variável de chave que você não tem, deixe fora (ausente é tratado como "não configurado", e funciona). Para descobrir as chaves reais: npx tsx scripts/sonda-cnn-prontidao.ts, que é somente leitura.

2 · Banco de dados

O banco Postgres é parte interna do serviço e é imutável para terceiros: somente o administrador da integração cria as tabelas e mantém a configuração de canais e funis. Quem consome a API não precisa — nem consegue — acessar o banco; tudo acontece pelos endpoints. (Administrador: o passo a passo do banco está em Adicionar um médico e em Colocando no ar.)

3 · Webhooks

São 3 webhooks — cada um é criar na tela do sistema de origem e adicionar a URL da API. O passo a passo campo a campo de cada tela está no guia docs/WEBHOOKS.md do repositório.

Onde criarEventoURL da APIAutenticação
SymplesDesk (2 canais)Mensagem criada/webhooks/symplesdesk-newmessageToken de autenticação
Pipedrive (Webhooks v2)deal: create + change/webhooks/pipedriveUsuário e senha (Basic Auth)
Pipedrive (Webhooks v2)person: change (sync de nome)/webhooks/pipedriveUsuário e senha (Basic Auth)
SymplesDesk (2 canais)Novo atendimento/webhooks/symplesdesk
SymplesDesk (2 canais)Oportunidade (card)/webhooks/oportunidadeToken de autenticação
SymplesDesk (2 canais)Atendimento finalizado/webhooks/atendimento-finalizadoToken de autenticação
Regra de ouro: cada evento deve ter um destino só. Se algum desses eventos já dispara para outra automação, troque a URL pela da API em vez de criar um webhook em paralelo — dois destinos processando o mesmo evento duplicam deals e fecham conversas duas vezes.
Crie o webhook só depois que a rota estiver no ar. O SymplesDesk desativa sozinho uma configuração que recebe falhas seguidas — já aconteceu aqui, com um webhook apontado para uma URL que ainda não existia.

Como adicionar um novo canal / médico administrador

Somente o administrador da integração executa estes passos — o banco é imutável para qualquer outra pessoa. Toda a configuração por médico é dado, não código: adicionar um médico novo é inserir linhas em duas tabelas, guardar um token no ambiente e criar os webhooks do canal. Nenhum deploy de código novo.

Passo 1 · Inserir o canal em public.integration_channels

Troque os valores em MAIÚSCULAS pelos reais (a tabela abaixo do SQL explica onde encontrar cada um):

INSERT INTO integration_channels
  (doctor_code, doctor_name, symplesdesk_account_id, symplesdesk_whatsapp_id,
   symplesdesk_base_url, symplesdesk_api_id, symplesdesk_token_secret_name,
   pipedrive_owner_id, pipedrive_pipeline_id, pipedrive_stage_id,
   pipedrive_label_id, is_active)
VALUES
  ('SIGLA', 'NOME DO MÉDICO', 'ACCOUNT_ID_SYMPLESDESK', 'WHATSAPP_ID',
   'https://serv2api.simplesdesk.com.br', 'API_ID_DA_CONFIG_DE_API',
   'SYMPLESDESK_TOKEN_SIGLA', 'OWNER_ID_PIPEDRIVE', 'ID_FUNIL_LEADS',
   'ID_ETAPA_ENTRADA', 'ID_ETIQUETA', true);
CampoO que é / onde encontrar
doctor_codeSigla única do médico (1 letra, ex.: 'T'). Aparece no título do deal (telefone - SIGLA) e amarra todas as outras configs.
doctor_nameNome de exibição (ex.: 'Dr. Thiago Delgado').
symplesdesk_account_idID da conta do canal no SymplesDesk (vem no payload dos webhooks do canal).
symplesdesk_whatsapp_idID do canal de WhatsApp no SymplesDesk (ex.: '139'). É por ele que o Fluxo 1 descobre de qual médico é o lead.
symplesdesk_base_urlSempre 'https://serv2api.simplesdesk.com.br' (base da API externa).
symplesdesk_api_idID (uuid) da configuração de API criada no SymplesDesk para este canal (tela APIs → a config de fechamento de conversa). Usado pelo Fluxo 2.
symplesdesk_token_secret_nameNome da variável de ambiente que guarda o token JWT dessa config — padrão 'SYMPLESDESK_TOKEN_SIGLA'. O token em si nunca vai no banco (só o nome da variável).
pipedrive_owner_idID do usuário do Pipedrive que será o dono dos deals criados (Configurações → Usuários).
pipedrive_pipeline_id / pipedrive_stage_idID do funil de Leads do médico e da etapa de entrada (aparecem na URL do Pipedrive ao abrir o funil/etapa).
pipedrive_label_idID da etiqueta aplicada à pessoa (produção usa a etiqueta "WhatsApp"). Pode ser NULL se não houver.
is_activetrue para ligar o canal; false desliga sem apagar a linha.

Passo 2 · Mapear os funis em public.integration_pipelines

Três linhas por médico — Leads (o funil onde o Fluxo 1 cria), Consulta e Procedimentos (funis que os Fluxos 2 e 3 rastreiam). Só o funil de Leads precisa de etapa:

INSERT INTO integration_pipelines
  (doctor_code, pipeline_type, pipedrive_pipeline_id, pipedrive_stage_id, is_active)
VALUES
  ('SIGLA', 'leads',         'ID_FUNIL_LEADS',         'ID_ETAPA_ENTRADA', true),
  ('SIGLA', 'consulta',      'ID_FUNIL_CONSULTA',      NULL,               true),
  ('SIGLA', 'procedimentos', 'ID_FUNIL_PROCEDIMENTOS', NULL,               true);

Passo 3 · Traduzir as etapas em public.integration_pipeline_steps

Sem esta tradução o card não acompanha o deal: o movimento é ignorado com registro, em silêncio. Uma linha por etapa, em cada funil que o médico novo vai usar (ver Mapa das etapas):

INSERT INTO integration_pipeline_steps
  (doctor_code, pipeline_type, step_name, symplesdesk_step_id, pipedrive_stage_id, step_order, is_active)
VALUES
  -- funil de Leads (SDR)
  ('SIGLA', 'leads',    'Cliente Potencial',    '52', 'ID_ETAPA_ENTRADA',    1, true),
  ('SIGLA', 'leads',    'Em conversa',          '50', 'ID_ETAPA_CONVERSA',   2, true),
  ('SIGLA', 'leads',    'Aguardando Pagamento', '48', 'ID_ETAPA_PAGAMENTO',  3, true),
  -- funil de Consulta (Concierge)
  ('SIGLA', 'consulta', 'Consulta Agendada',    '57', 'ID_ETAPA_AGENDADA',   1, true),
  ('SIGLA', 'consulta', 'Reagendamento',        '58', 'ID_ETAPA_REAGENDA',   2, true),
  ('SIGLA', 'consulta', 'Consulta Confirmada',  '59', 'ID_ETAPA_CONFIRMADA', 3, true);

As etapas do quadro (52, 50, 48 em Leads; 57, 58, 59 em Consulta) são as mesmas para todos os médicos — o quadro é único. O que muda por médico são os ids das etapas do funil no Pipedrive. A coluna pipeline_type é o que impede os números de se confundirem entre um funil e outro — nunca deixe de preenchê-la.

Passo 4 · Guardar o token no ambiente

No .env do serviço, crie a variável com o mesmo nome usado em symplesdesk_token_secret_name, com o token JWT gerado na config de API do canal:

SYMPLESDESK_TOKEN_SIGLA=<token JWT gerado na tela de APIs do SymplesDesk>

Passo 5 · Criar os webhooks do canal novo

Passo 6 · Reiniciar o serviço

A configuração de canais e funis é carregada na inicialização (fica em memória para não consultar o banco a cada mensagem). Depois de inserir as linhas, reinicie o serviço para o canal novo valer.

Conferência rápida: mande uma mensagem de teste pro canal novo e confira o deal criado no funil certo, com o dono certo e o título telefone - SIGLA. Qualquer erro fica registrado em integration_events.

Endpoints

Se você vai integrar com a API, esta é a sua seção. Toda interação de fora é por HTTP — ninguém além do administrador acessa o banco.

EndpointAutenticaçãoO que faz
GET /healthConfirma que o serviço está no ar ({"ok":true})
POST /webhooks/symplesdesk— (limitação da tela antiga)Fluxo 1: entrada de lead
POST /webhooks/pipedriveBasic AuthFluxos 2, 3 e 5-B: status, captura e movimentação de deals + sync de nome do contato (person)
POST /webhooks/symplesdesk-newmessageToken no headerFluxo 4: ingestão de cada mensagem
POST /webhooks/oportunidadeToken no headerCards: movimento, ganho, perda e renome feitos no quadro
POST /webhooks/atendimento-finalizadoToken no headerRegistra que alguém encerrou o atendimento na mão
GET /docsEsta página

Todos os webhooks respondem 200 mesmo em erro interno (o erro fica registrado no banco, em integration_events): se respondessem erro, o remetente reenviaria o evento e criaria duplicatas. A exceção é autenticação inválida, que responde 401 — ou 404 nas duas rotas mais novas, que não confirmam sequer a própria existência para quem não tem o token.

Cada resposta traz um outcome dizendo o que aconteceu (ex.: created, duplicate_ticket, active_session_exists, ignored_event) — a lista completa por endpoint está no docs/API.md do repositório.

Colocando no ar — passo a passo administrador

Estado atual: os 4 fluxos estão completos, com verificação independente e teste ponta a ponta com mensagens e conversas reais (22/07/2026).
  1. Deploy do serviço com o .env completo → gera a URL pública fixa da API.
  2. Preparar o banco: rodar db/schema.sql + db/seed_prod.sql (limpo — os dados atuais são de teste).
  3. Backfill: importar o histórico de deals do Pipedrive para o banco (npm run backfill, com --dry-run antes para conferir os números).
  4. Criar o webhook "Mensagem criada" nos 2 canais do SymplesDesk com a URL da API (Fluxo 4).
  5. Criar o webhook de deals no Pipedrive e apontar o evento de novo atendimento dos 2 canais para a URL da API (Fluxos 1, 2 e 3) — lembrando a regra de ouro: um destino por evento.
  6. Validar com um atendimento real e acompanhar integration_events e os alertas do Discord.