Pipeline (CRM)
Gerencie pipelines de vendas completos com estágios customizáveis, cards (deals), automações de workflow e lead scoring integrado.
Base URL
Todos os endpoints usam o prefixo /api/v1/accounts/{account_id}
Isolamento multi-tenant e segurança
Toda a API de Pipeline é escopada por conta. Você nunca consegue ler, alterar ou apagar dados de outra conta — nem por engano, nem de propósito. Entenda como o isolamento funciona antes de automatizar operações destrutivas.
O account_id vem da URL, nunca do corpo da requisição
O {account_id} faz parte do caminho da URL e é validado contra o seu token: a API confirma que o usuário autenticado é membro daquela conta antes de qualquer operação (caso contrário, 401). Nenhum endpoint lê account_id do corpo (body) da requisição. Por isso não existe o cenário de "esquecer de setar o account_id" e atingir outra conta — o escopo é sempre derivado da rota autenticada.
IDs no caminho são resolvidos dentro da sua conta
Todos os identificadores em {id}, display_id e conversation_display_id são buscados dentro do escopo da conta da URL(Current.account.pipeline_cards.find(...)). Um ID que pertence a outra conta retorna 404 Not Found — nunca o registro alheio.
Atenção: display_id e conversation_display_id são sequências por conta(a conta A e a conta B podem ambas ter o card #42), não identificadores globais. Use sempre o ID retornado pelos endpoints da sua própria conta; não tente adivinhar/iterar IDs.
Permissões por operação
- Pipelines: agentes podem consultar os pipelines visíveis; criar, atualizar e remover pipelines exige administrador.
- Cards: agentes podem consultar, criar, atualizar, mover e reordenar apenas em pipelines dos quais são membros. Excluir, acessar/restaurar a lixeira, excluir permanentemente, importar CSV e usar
bulk_assignexige administrador. - Anexos: agentes e administradores podem listar, baixar e enviar em cards visíveis; remover um anexo exige administrador.
- Automações do Pipeline: leitura respeita a visibilidade do pipeline; criar, atualizar, remover, duplicar, executar e usar
dry_runexige administrador. Webhooks do Pipeline são inteiramente administrativos.
Falhas de permissão podem aparecer como 401 nos controllers legados e como 403 no namespace /pipeline.
Delete não cascateia entre contas — e é soft por padrão
DELETE /pipeline_cards/{id}é um soft delete (marcadiscarded_at), reversível. O hard delete exige um passo separado e explícito (permanently_delete, restrito a administradores e só para cards já descartados).DELETE /pipelines/{id}faz soft delete apenas dos cards daquela conta — nunca dispara uma cascata atingindo cards de outras contas.- Operações em lote (
bulk) ereorderfiltram os IDs recebidos pela sua conta antes de agir: qualquer ID de outra conta é silenciosamente ignorado (no-op), nunca apagado nem movido. Oreorderainda exige umpipeline_idde escopo para reforçar o isolamento entre pipelines: use o campo raiz canônico; por compatibilidade, o backend aceita opipeline_idda primeira posição como fallback legado.
Pipelines
/api/v1/accounts/{account_id}/pipelinesLista os pipelines visíveis ao usuário autenticado; administradores veem todos os pipelines da conta.
curl -s "https://chat.seudominio.com/api/v1/accounts/1/pipelines" \
-H "api_access_token: YOUR_TOKEN" | jq .[
{
"id": 1,
"account_id": 1,
"name": "Vendas B2B",
"description": "Pipeline principal de vendas",
"active": true,
"stages": {
"1_prospeccao": { "id": "1_prospeccao", "name": "Prospeccao", "color": "#3b82f6", "position": 0 },
"1_proposta": { "id": "1_proposta", "name": "Proposta", "color": "#f59e0b", "position": 1 },
"1_ganho": { "id": "1_ganho", "name": "Ganho", "color": "#10b981", "position": 2, "is_won_stage": true },
"1_perdido": { "id": "1_perdido", "name": "Perdido", "color": "#ef4444", "position": 3, "is_lost_stage": true }
},
"settings": { "require_won_value": false, "require_loss_reason": false },
"inbox_id": null,
"default_for_appointments": false,
"created_at": "2025-06-01T00:00:00Z",
"updated_at": "2025-06-01T00:00:00Z"
}
]/api/v1/accounts/{account_id}/pipelinesCria um novo pipeline com estágios.
stages é obrigatório; prefira o objeto por ID
O campo stages é obrigatório. Envie pelo menos um estágio e use, como formato canônico, um objeto (hash) por ID. Arrays ainda são aceitos por compatibilidade, mas itens sem id recebem uma chave numérica de fallback; não dependa desse caminho legado. Use sempre Content-Type: application/json.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
pipeline.name | string | Sim | Nome do pipeline |
pipeline.description | string | Nao | Descrição |
pipeline.active | boolean | Nao | Status de ativação |
pipeline.inbox_id | integer | Nao | Inbox associada ao pipeline |
pipeline.stage_sla_enabled | boolean | Nao | Ativa o SLA por estágio |
pipeline.default_for_appointments | boolean | Nao | Marca este funil como destino padrão para agendamentos criados sem funil explícito. Marcar um funil desmarca automaticamente o que segurava o posto na mesma gravação; apenas um funil por conta pode ser default |
pipeline.agents | array | Nao | Agentes membros do pipeline; o backend incorpora a lista em settings.agents |
pipeline.settings | object | Nao | Configurações allowlisted do pipeline (max 64 KB) |
pipeline.stages | object | Sim | Objeto com estágios. Chaves são IDs temporários (string). Após criação os IDs são normalizados para {pipeline_id}_{slug} |
pipeline.stages.{key}.name | string | Sim | Nome do estágio |
pipeline.stages.{key}.color | string | Nao | Cor em hex (ex: "#3B82F6") |
pipeline.stages.{key}.icon | string | Nao | Ícone do estágio |
pipeline.stages.{key}.position | integer | Nao | Ordem crescente de exibição; o menor valor aparece primeiro (a interface normalmente inicia em 0) |
pipeline.stages.{key}.description | string | Nao | Descrição do estágio |
pipeline.stages.{key}.is_entry_stage | boolean | Nao | Marca o estágio de entrada |
pipeline.stages.{key}.is_won_stage | boolean | Nao | Marca o estágio terminal de ganho |
pipeline.stages.{key}.is_lost_stage | boolean | Nao | Marca o estágio terminal de perda |
pipeline.stages.{key}.automation_actions | JSON | Nao | Ações de automação associadas ao estágio |
pipeline.stages.{key}.wip_limit | integer | Nao | Limite de cards em andamento |
pipeline.stages.{key}.sla_hours | number | Nao | Prazo de SLA do estágio, em horas |
pipeline.stages.{key}.message_templates | array | Nao | Templates de mensagem associados ao estágio |
Chaves aceitas em pipeline.settings
card_display_fields, automations, notifications, visibility, sort_order, default_filter, quick_values, label_stage_mappings, stage_label_mappings, auto_assignment, require_loss_reason, enable_won_lost_stages, require_won_value, lead_scoring, agents, auto_archive_on_resolved e auto_win_on_resolved. Outras chaves são descartadas.
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipelines" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline": {
"name": "Vendas B2B",
"stages": {
"stage_1": { "name": "Prospeccao", "color": "#6366f1", "position": 0 },
"stage_2": { "name": "Qualificação", "color": "#3B82F6", "position": 1 },
"stage_3": { "name": "Proposta", "color": "#f59e0b", "position": 2 },
"stage_4": { "name": "Fechamento", "color": "#10b981", "position": 3 }
}
}
}'IDs dos estágios após criação
Após criar o pipeline, os IDs dos estágios são normalizados pelo servidor para o formato {pipeline_id}_{slug_do_nome} (ex: 1_prospeccao). Use GET /pipelines/{id}/stages para obter os IDs reais antes de criar cards.
/api/v1/accounts/{account_id}/pipelines/{id}Atualiza nome, descrição, estágios ou settings de um pipeline.
ATENÇÃO: formato do campo stages
Sempre envie stages como objeto (hash) com IDs reais como chaves. Arrays são aceitos por compatibilidade e preservam id quando ele existe; sem id, o backend usa o índice numérico como fallback. Esse payload não é rejeitado de forma geral e pode fazer estágios existentes parecerem removidos, movendo cards órfãos. Portanto, não envie arrays sem IDs.
A rota PATCH /pipelines/{id}/stages/{stage_id} não existe e retorna 404. Para editar uma stage individual (descrição, cor, nome, posição), envie PATCH /pipelines/{id} com o hashstagescompleto — o backend faz diff inteligente.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
pipeline.name | string | Nao | Nome do pipeline |
pipeline.description | string | Nao | Descrição |
pipeline.active | boolean | Nao | Status de ativação |
pipeline.inbox_id | integer | Nao | Inbox associada ao pipeline |
pipeline.stage_sla_enabled | boolean | Nao | Ativa o SLA por estágio |
pipeline.default_for_appointments | boolean | Nao | Marca este funil como destino padrão para agendamentos criados sem funil explícito. Marcar um funil desmarca automaticamente o que segurava o posto na mesma gravação; apenas um funil por conta pode ser default |
pipeline.agents | array | Nao | Agentes membros; mesclados em settings.agents |
pipeline.stages | object | Nao | Hash com stages keyed por ID real (ex: "1_qualificado"). Cada estágio aceita os mesmos campos da tabela de criação. |
pipeline.settings | object | Nao | Configurações allowlisted do pipeline (JSONB). Chaves desconhecidas são descartadas; o objeto enviado substitui o settings persistido. |
settings e agents não fazem deep-merge
Quando pipeline.settings é enviado, o objeto allowlisted substitui o blob atual. Enviar apenas pipeline.agents também constrói um novo settings com essa lista. Envie todas as configurações que devem permanecer; um payload parcial pode remover membros ou outras preferências.
curl -X PATCH "https://chat.seudominio.com/api/v1/accounts/1/pipelines/36" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline": {
"stages": {
"36_qualificado": {
"name": "Qualificado",
"position": 1,
"description": "Lead qualificado para próxima etapa",
"color": "#10b981"
},
"36_entrada_de_lead": { "name": "Entrada de Lead", "position": 0 }
}
}
}'Comportamento ao remover stages
Quando uma stage existente é omitida do payload, ela é considerada removida e os cards que estavam nela são automaticamente movidos para a primeira stage não-terminal por ordem de position. Cada movimento é registrado em pipeline_stage_histories.
Limites: máximo de 50 stages por pipeline; settings JSON até 64 KB.
/api/v1/accounts/{account_id}/pipelines/{id}Remove um pipeline. Os cards associados são soft-deleted (discard), não apagados.
Escopo de conta e cascata
O id é resolvido dentro da conta autenticada(Current.account.pipelines.find) — não é um ID global. Tentar deletar um pipeline de outra conta retorna 404.
Ao destruir o pipeline, os cards associados são soft-deleted(discard!, marcando discarded_at) — não são hard-deleted — e pipeline_stage_histories, follow_up_rules e pipeline_webhooks são removidos em cascata. O próprio registro do pipeline é removido permanentemente; planeje com cuidado antes de chamar.
Estágios do Pipeline
/api/v1/accounts/{account_id}/pipelines/{pipeline_id}/stagesLista os estágios de um pipeline. O id de cada estágio é a string no formato {pipeline_id}_{slug} usada no campo pipeline_stage dos cards.
{
"pipeline_id": 8143,
"pipeline_name": "Clinica Medica - Jornada do Paciente",
"stages": [
{
"id": "8143_entrada_de_lead",
"name": "Entrada de Lead",
"color": "#64748b",
"icon": null,
"position": 0,
"description": "Entrada automática de leads via inbox",
"is_entry_stage": true,
"is_won_stage": false,
"is_lost_stage": false
},
{
"id": "8143_ganho",
"name": "Ganho",
"color": "#10b981",
"position": 6,
"is_entry_stage": false,
"is_won_stage": true,
"is_lost_stage": false
}
]
}Renomear um estágio muda o id (pipeline_stage)
O id do estágio é derivado do nome ({pipeline_id}_{slug}). Ao renomear um estágio via PATCH /pipelines/{id}, o backend re-slugifica a chave e migra os cards daquele estágio na mesma transação — ou seja, o valor de pipeline_stage muda. Integrações que armazenam o id do estágio devem reconsultar GET /pipelines/{id}/stages após mudanças de configuração.
Cards (Deals)
Cards representam oportunidades de venda (deals) dentro de um pipeline. Cada card possui valor, contato associado, estágio atual e lead score.
/api/v1/accounts/{account_id}/pipeline_cardsLista cards do kanban com filtros. Existem duas rotas: /pipeline_cards (legacy, paginação por cursor ou offset) e /pipeline/cards (canônica, paginação page/per_page). Ambas aceitam o mesmo conjunto completo de filtros avançados, incluindo busca textual server-side, via o concern compartilhado PipelineCardFilterable.
Filtros avançados (ambas as rotas, via concern compartilhado)
A rota /api/v1/accounts/{id}/pipeline_cards (legacy, paginação por limit com cursor ou offset) aceita o conjunto completo de filtros: além de pipeline_id, pipeline_stage, conversation_display_id e contact_id, ela também filtra por search, labels[], priority[], value_min/value_max, agent_id, date_start/date_end, status, sla_exceeded e stages[]. Os mesmos filtros são aplicados pelo endpoint de exportação (GET /pipeline/cards/export).
A rota /api/v1/accounts/{id}/pipeline/cards (canônica, paginação por page/per_page) aceita os mesmos filtros avançados (mesmo concern), e adicionalmente owner_id e lead_score_category (hot/warm/cold).
O parâmetro search faz busca textual server-side por título/descrição do card, contato, responsável, inbox, identificadores e nome do estágio. O termo é limitado aos primeiros 200 caracteres, pode ser combinado com os demais filtros e sempre respeita a conta autenticada e a visibilidade do usuário.
Importante: o filtro contact_id foi implementado na v4.12.3.0. Antes dessa versão, ele era silenciosamente ignorado e retornava todos os cards da conta. Se você tem automações que dependem desse filtro, confirme que está rodando v4.12.3.0 ou superior.
Parâmetros (rota legacy /pipeline_cards — conjunto completo)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
pipeline_id(query) | integer | Nao | Filtrar por pipeline (todos os cards desse funnel) |
pipeline_stage(query) | string | Nao | Filtrar por um estágio específico (ex: "1_lead") |
conversation_display_id(query) | integer | Nao | Filtrar por conversa associada (display_id, não ID global) |
contact_id(query) | integer | Nao | Filtrar por contato (v4.12.3.0+) |
search(query) | string | Nao | Busca textual server-side (max 200 caracteres) por card, contato, responsável, inbox, identificadores ou estágio. |
labels[](query) | string[] | Nao | Títulos de labels da conversa vinculada ao card. Match OR (qualquer um dos títulos). |
priority[](query) | string[] | Nao | Prioridade do card: none, low, medium, high, urgent. Match OR. |
value_min(query) | number | Nao | Valor mínimo do card (>=). Valor do card = expected_revenue ou item_details.value. Cards sem valor algum são EXCLUÍDOS quando value_min/value_max está presente. |
value_max(query) | number | Nao | Valor máximo do card (<=). Cards sem valor algum são excluídos (filtrar por valor implica ter valor). |
agent_id(query) | integer | Nao | ID do responsável (owner) do card. Use -1 ou "unassigned" para cards sem responsável. |
date_start(query) | string | Nao | Data inicial de criação (created_at), formato YYYY-MM-DD, interpretada no fuso da conta. |
date_end(query) | string | Nao | Data final de criação (created_at), formato YYYY-MM-DD, fuso da conta. |
status(query) | string | Nao | Estado do deal: open, won, lost ou closed. |
sla_exceeded(query) | boolean | Nao | true = apenas cards abertos com SLA vencido (sla_overdue). |
stages[](query) | string[] | Nao | Lista de estágios (pipeline_stage). Match OR — cards em qualquer um dos estágios informados. |
limit(query) | integer | Nao | Itens por página (default 50). O valor é limitado ao intervalo de 1 a 500. |
cursor(query) | string | Nao | Cursor opaco retornado em meta.next_cursor. Use para buscar a próxima página sem recalcular offset. |
offset(query) | integer | Nao | Offset de compatibilidade, usado quando cursor não é enviado.(default: 0) |
Parâmetros extras da rota canônica /pipeline/cards (além dos filtros avançados acima)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
pipeline_id(query) | integer | Nao | Filtrar por pipeline |
pipeline_stage(query) | string | Nao | Filtrar por estágio (ex: "69_lead") |
owner_id(query) | integer | Nao | Filtrar por responsável (além de agent_id da tabela acima) |
contact_id(query) | integer | Nao | Filtrar por contato (v4.12.3.0+) |
lead_score_category(query) | string | Nao | hot, warm, cold (exclusivo da canônica) |
page(query) | integer | Nao | Página atual(default: 1) |
per_page(query) | integer | Nao | Itens por página (máximo 100)(default: 25) |
Como percorrer todos os cards com cursor
Na primeira chamada, omita cursor. Enquanto meta.has_more for true, repita a requisição com os mesmos filtros e envie cursor=meta.next_cursor. Pare quando has_more=false. Trate o cursor como opaco; não tente montá-lo ou interpretá-lo no cliente.
# Filtros avancados na rota legacy: prioridade alta, valor minimo, SLA vencido
curl -s "https://chat.seudominio.com/api/v1/accounts/1/pipeline_cards?pipeline_id=1&priority[]=high&priority[]=urgent&value_min=5000&sla_exceeded=true" \
-H "api_access_token: YOUR_TOKEN" | jq .
# Filtrar por labels, agente e intervalo de datas
curl -s "https://chat.seudominio.com/api/v1/accounts/1/pipeline_cards?labels[]=vip&agent_id=5&date_start=2026-01-01&date_end=2026-06-30" \
-H "api_access_token: YOUR_TOKEN" | jq .
# Cards sem responsavel
curl -s "https://chat.seudominio.com/api/v1/accounts/1/pipeline_cards?agent_id=unassigned&pipeline_id=1" \
-H "api_access_token: YOUR_TOKEN" | jq .
# Canonica com status
curl -s "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards?status=open&pipeline_id=1" \
-H "api_access_token: YOUR_TOKEN" | jq .{
"data": [
{
"id": 1,
"title": "Contrato Empresa XYZ",
"pipeline_id": 1,
"pipeline_stage": "1_qualificacao",
"position": 1,
"status": "open",
"contact_id": 10,
"owner_id": 5,
"lead_score": 78,
"conversation_display_id": 42,
"item_details": { "value": 15000.00 },
"active_sequence": { "id": 9, "name": "Onboarding", "status": "active", "current_step": 1, "total_steps": 3, "progress_percentage": 33 },
"created_at": "2026-01-10T10:00:00Z"
}
],
"meta": {
"limit": 50,
"offset": 0,
"has_more": false,
"next_cursor": null,
"stage_counts": { "1_lead": 12, "1_qualificacao": 18, "1_ganho": 12 }
}
}Importar Cards (CSV)
Importe cards (deals) em lote a partir de um arquivo CSV. O processamento é assíncrono: a API valida e enfileira o import, e o job cria os cards em background. Ambos os endpoints exigem permissão de administrador.
/api/v1/accounts/{account_id}/pipeline/cards/templateBaixa um CSV modelo com os cabeçalhos esperados e uma linha de exemplo.
Formato do CSV modelo
A resposta é um arquivo text/csv (download direto — Content-Disposition: attachment, filename pipeline_cards_import_template.csv). A primeira linha contém os cabeçalhos e a segunda linha traz um exemplo preenchido.
Colunas (nessa ordem):
title— obrigatória, título do card.stage— estágio inicial do card. Aceita a chave completa (1_contato), o slug (contato) ou o nome legível sem distinção de caixa/acento (Contato,Negociação). Não reconhecido → primeira etapa do pipeline.description— descrição livre.expected_revenue— receita esperada (numérico).contact_identifier— identificador externo do contato.contact_email— email do contato.contact_phone— telefone do contato.
curl -s "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/template" \
-H "api_access_token: YOUR_TOKEN" \
-o pipeline_cards_import_template.csv/api/v1/accounts/{account_id}/pipeline/cards/importEnvia um CSV para importar cards em lote (multipart/form-data). Processamento assíncrono.
multipart/form-data — campos obrigatórios
Envie como multipart/form-data. O arquivo CSV vai no campo import_file e o pipeline alvo no campo pipeline_id. O pipeline_id é resolvido dentro da conta autenticada — um pipeline de outra conta (ou ausente) retorna 422.
Validação do arquivo (limites)
O upload é validado antes de enfileirar: tamanho máximo de 10MB; content-type aceito text/csv, text/plain, application/csv ou application/vnd.ms-excel; e a primeira linha precisa conter o cabeçalho do template (coluna title). Arquivo fora dessas regras retorna 422.
Body (multipart/form-data)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
import_file(body) | file | Sim | Arquivo CSV no formato do template (cabeçalhos na 1ª linha). Máximo 10MB. |
pipeline_id(body) | integer | Sim | ID do pipeline alvo onde os cards serão criados (escopado à conta). |
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/import" \
-H "api_access_token: YOUR_TOKEN" \
-F "import_file=@pipeline_cards.csv" \
-F "pipeline_id=1"{
"message": "Importação iniciada. Os cards serão criados em segundo plano.",
"import_id": 42
}{ "error": "Pipeline obrigatorio para importar cards." }Exportar Cards (CSV)
Exporta os cards da conta como um arquivo CSV, respeitando os mesmos filtros da listagem legacy /pipeline_cards. O resultado é escopado à conta autenticada (a visibilidade por pipeline do usuário também se aplica).
/api/v1/accounts/{account_id}/pipeline/cards/exportExporta os cards filtrados como CSV (download direto). Aceita os query params da listagem legacy /pipeline_cards.
Filtros aceitos
Os mesmos filtros da rota legacy de listagem se aplicam (query params): pipeline_id, pipeline_stage, conversation_display_id, contact_id, labels[], priority[], value_min/value_max, agent_id, date_start/date_end, status, sla_exceeded, stages[] e search. Sem filtros, exporta todos os cards visíveis da conta.
A resposta é um arquivo text/csv (Content-Disposition: attachment, filename pipeline_cards_export_<timestamp>.csv, com o timestamp no fuso da conta).
Filtros exclusivos da rota canônica
owner_id e lead_score_category são filtros exclusivos da listagem canônica e não são aceitos pelo export. Para filtrar por responsável no CSV, use agent_id.
Limite de linhas e proteção anti-formula
O export síncrono é limitado a 20.000 cards. Acima disso a API retorna 422 — aplique filtros para reduzir o resultado.
Células de texto que começam com =, @, + ou - (e não são números/telefones) são prefixadas com apóstrofo (') para neutralizar injeção de fórmula em planilhas (CSV injection).
Colunas do CSV exportado
id, title, pipeline_id, stage, status, priority, value, currency, owner_name, contact_name, contact_email, contact_phone, conversation_display_id, labels, lead_score, lead_score_category, created_at, won_at, lost_at, deadline. Datas em ISO 8601 no fuso da conta; labels são separadas por |.
# Exportar cards abertos de um pipeline que correspondem a busca textual
curl -s "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/export?pipeline_id=1&status=open&search=Acme" \
-H "api_access_token: YOUR_TOKEN" \
-o pipeline_cards_export.csv/api/v1/accounts/{account_id}/pipeline_cardsCria um novo card (deal) no pipeline.
Como obter o pipeline_stage
O campo pipeline_stage é uma string no formato {pipeline_id}_{slug} (ex: "1_prospeccao"). Use GET /pipelines/{id}/stages para listar os IDs reais dos estágios do seu pipeline antes de criar cards.
Body envolto em pipeline_card
Envie sempre os campos dentro do wrapper pipeline_card. A rota legacy /pipeline_cards tolera campos no nível raiz (são re-embrulhados automaticamente via wrap_parameters e o card é criado normalmente), mas a rota canônica POST /pipeline/cards é estrita: sem o wrapper ela retorna 422 (param is missing: pipeline_card). Use sempre o wrapper para um comportamento consistente entre as duas rotas.
Body (pipeline_card)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
title | string | Nao | Título do deal no nível raiz. Opcional — se omitido, o card usa item_details.title (ou fica null). |
pipeline_id | integer | Sim | ID do pipeline |
pipeline_stage | string | Sim | ID do estágio no formato "{pipeline_id}_{slug}" (ex: "1_prospeccao"). Obtenha via GET /pipelines/{id}/stages. Precisa ser um estágio comum: nascer em etapa de Ganho/Perda retorna 422 (veja abaixo). |
position | integer | Nao | Posição do card no estágio |
contact_id | integer | Nao | ID do contato associado (resolvido dentro da conta autenticada) |
conversation_display_id | integer | Nao | Display ID da conversa associada (per-account, não ID global) |
owner_id | integer | Nao | ID do responsável |
timer_started_at | string | Nao | Início do timer (ISO 8601) |
timer_duration | integer | Nao | Duração acumulada do timer |
scheduled_at | string | Nao | Data/hora agendada (ISO 8601; rota legacy) |
expected_revenue | number | Nao | Receita esperada do deal |
currency | string | Nao | Código de moeda com exatamente 3 letras, armazenado em maiúsculas (ex: BRL, USD). O backend valida o formato, não a existência em um catálogo oficial. |
deadline | string | Nao | Data prevista de fechamento (ISO 8601) |
forecast_close_date | string | Nao | Previsão de fechamento (ISO 8601; rota legacy) |
description | string | Nao | Descrição do card |
priority | string | Nao | Prioridade do card |
source | string | Nao | Origem do card (rota legacy) |
item_details | object | Nao | JSONB freeform. value e currency são espelhos legados; prefira expected_revenue e currency no nível raiz. |
custom_attributes | object | Nao | Atributos personalizados |
qualification_checklist | object | Nao | Checklist de qualificação |
tags | string[] | Nao | Tags do card |
Schemas diferentes nas duas rotas de cards
A tabela acima descreve a rota legacy /pipeline_cards. A rota canônica /pipeline/cards também aceita os campos comuns, adiciona probability e company_id, mas não aceita os campos legacy scheduled_at, forecast_close_date e source. Não reutilize um payload entre as duas rotas sem considerar essa diferença.
Um card não nasce fechado
pipeline_stage aqui precisa apontar para um estágio comum. Criar um card diretamente em uma etapa de Ganho ou Perda retorna 422: esse atalho gravava o card como fechado sem passar pelo fechamento real — sem valor fechado, sem won_at/lost_at e sem o registro da oportunidade no ledger, deixando a receita divergente do que o board mostra.
Para importar um deal já fechado, crie o card em um estágio comum e feche-o em seguida com POST .../deal_status/mark_won ou POST .../deal_status/mark_lost.
{
"errors": {
"pipeline_stage": [
"can only reach a won/lost stage through the deal endpoints (mark_won / mark_lost / reopen)"
]
}
}# Primeiro obtenha os IDs dos estágios:
# curl -s ".../api/v1/accounts/1/pipelines/1/stages" -H "api_access_token: TOKEN"
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline_cards" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline_card": {
"title": "Contrato Empresa XYZ",
"pipeline_id": 1,
"pipeline_stage": "1_prospeccao",
"contact_id": 10,
"owner_id": 5,
"expected_revenue": 15000.00,
"currency": "BRL",
"item_details": {
"title": "Contrato Empresa XYZ"
},
"deadline": "2026-03-15T00:00:00Z"
}
}'/api/v1/accounts/{account_id}/pipeline_cards/{id}Retorna os detalhes e o score de um card. A timeline não faz parte desta resposta; consulte GET /pipeline/cards/{id}/timeline.
/api/v1/accounts/{account_id}/pipeline_cards/{id}Atualiza dados de um card. Body envolto em pipeline_card.
Body (pipeline_card)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
title | string | Nao | Título do card |
pipeline_stage | string | Nao | Estágio (formato "{pipeline_id}_{slug}"). Para mover entre estágios prefira POST /move_to_stage. Entrar ou sair de uma etapa de Ganho/Perda por aqui retorna 422 (veja abaixo). |
pipeline_id | integer | Nao | ID do pipeline |
position | integer | Nao | Posição do card no estágio |
owner_id | integer | Nao | ID do responsável |
contact_id | integer | Nao | ID do contato |
conversation_display_id | integer | Nao | Display ID da conversa associada |
timer_started_at | string | Nao | Início do timer (ISO 8601) |
timer_duration | integer | Nao | Duração acumulada do timer |
scheduled_at | string | Nao | Data/hora agendada (ISO 8601; rota legacy) |
expected_revenue | number | Nao | Receita esperada (coluna do card) |
currency | string | Nao | Código de moeda com exatamente 3 letras, armazenado em maiúsculas (ex: BRL, USD). Valida o formato, não um catálogo oficial. |
deadline | string | Nao | Data prevista de fechamento (ISO 8601) |
forecast_close_date | string | Nao | Data de previsão de fechamento (ISO 8601) |
description | string | Nao | Descrição do card |
priority | string | Nao | Prioridade do card |
source | string | Nao | Origem do card (rota legacy) |
item_details | object | Nao | JSONB freeform. value, currency e deadline_at são espelhos/aliases legados. Faz deep-merge com o valor atual — envie apenas as chaves a alterar. |
custom_attributes | object | Nao | Atributos personalizados (JSONB) |
qualification_checklist | object | Nao | Checklist de qualificação |
tags | string[] | Nao | Tags do card |
Status usa endpoints de transição dedicados
O campo pipeline_card.status não é aceito pelo PATCH genérico. Ele é removido dos parâmetros permitidos, portanto a requisição pode retornar 200 sem alterar o status. Use exclusivamente POST .../deal_status/mark_won, POST .../deal_status/mark_lost ou POST .../deal_status/reopen para executar a transição completa.
Fechar e reabrir um deal não passa pelo PATCH
Bloquear apenas status não bastava: mudar o pipeline_stage já fechava o deal por tabela, pulando tudo que o fechamento garante (valor fechado, won_at/lost_at, lost_reason e o registro da oportunidade no ledger). Por isso o PATCH genérico agora recusa a transição nos dois sentidos, com 422:
- Entrar em uma etapa de Ganho/Perda (inclusive quando a mudança vem de trocar o
pipeline_idmantendo a mesma chave de estágio) — usePOST .../deal_status/mark_wonouPOST .../deal_status/mark_lost. - Sair de uma etapa de Ganho/Perda com o deal ainda fechado — use
POST .../deal_status/reopen. Arrastar o card de volta pelo PATCH o mostrava aberto no board enquantostatus,won_ate a oportunidade continuavam fechados.
POST .../move_to_stage não foi afetado: ele detecta a etapa de destino e executa o fechamento completo por conta própria. Continua sendo o caminho recomendado para mover cards, inclusive para etapas terminais.
{
"errors": {
"pipeline_stage": [
"can only reach a won/lost stage through the deal endpoints (mark_won / mark_lost / reopen)"
]
}
}Campos financeiros canônicos e espelhos legados
Use os campos raiz expected_revenue e currency como fonte canônica. O backend mantém os espelhos legados em item_details sincronizados automaticamente.
expected_revenue— coluna numérica canônica do card (receita esperada). Use este para o valor previsto do deal.currency— código com exatamente três letras, armazenado em maiúsculas. A validação confirma o formato, mas não consulta um catálogo oficial de moedas.item_details.valueeitem_details.currency— espelhos legados em JSONB. Quando o payload envia campos raiz e espelhos com valores diferentes, os campos raiz explícitos têm precedência e sobrescrevem os espelhos.won_value— preenchido apenas ao fechar o deal viaPOST .../deal_status/mark_won(valor fechado).
Se item_details.offers estiver presente, todas as ofertas devem usar a mesma moeda canônica do card. Valores financeiros malformados, código de moeda fora do formato de três letras ou ofertas com moedas diferentes retornam 422 Unprocessable Entity.
curl -X PATCH "https://chat.seudominio.com/api/v1/accounts/1/pipeline_cards/5" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline_card": {
"title": "Contrato Empresa XYZ - revisado",
"owner_id": 5,
"expected_revenue": 18000,
"currency": "BRL"
}
}'/api/v1/accounts/{account_id}/pipeline_cards/{id}Remove um card (soft delete).
Soft delete escopado à conta
O id é resolvido dentro da conta autenticada(Current.account.pipeline_cards.find). Não é um ID global — tentar deletar um card de outra conta retorna 404. A operação é um soft delete (discarded_at); o card pode ser visto em GET /pipeline/cards/discarded e restaurado via POST /pipeline/cards/{id}/restore. A remoção definitiva exige um passo separado e explícito (DELETE /pipeline/cards/{id}/permanently_delete, só para cards já descartados).
Lixeira por pipeline: GET /pipeline/cards/discarded é admin-only e aceita o parâmetro opcional pipeline_id(filtra a lixeira daquele pipeline; pipeline_id inválido/de outra conta retorna 404). Sem pipeline_id, lista a lixeira da conta inteira. Cada card retorna discarded_at, discarded_by, discard_reason e days_until_permanent_deletion (contagem regressiva até a exclusão definitiva, respeitando a retenção da conta — default 30 dias).
Contatos e Conversas adicionais (multi-vínculo)
Além do contato e da conversa primários do card (contact_id / conversation_display_id), um card pode ter contatos e conversas adicionais vinculados. Isso permite representar um deal com vários envolvidos (ex: decisor + influenciador) ou várias conversas relacionadas. Os vínculos primários não mudam — estes endpoints apenas adicionam/removem vínculos extras (join aditivo, many-to-many).
Aditivo e retrocompatível — o vínculo primário não muda
O contato primário continua em contact_id/contact e a conversa primária em conversation_display_id. Os vínculos adicionais aparecem em dois novos campos aditivos do recurso pipeline_card: additional_contacts e additional_conversations. O vínculo primário é deduplicado para fora desses arrays (nunca aparece duas vezes).
Novos campos no recurso do card
{
"id": 5,
"contact_id": 10,
"conversation_display_id": 42,
"additional_contacts": [
{
"id": 3,
"contact_id": 27,
"name": "Joao Comprador",
"email": "joao@empresa.com",
"phone_number": "+5511999999999",
"avatar_url": "",
"role": "decisor"
}
],
"additional_conversations": [
{ "id": 8, "conversation_display_id": 57 }
]
}id do vínculo != id do recurso
Em additional_contacts/additional_conversations, o campo id é o ID do registro de vínculo (join), não o contact_id nem o conversation_display_id. Use esse id de vínculo nas rotas DELETE abaixo.
Contatos adicionais
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/contactsVincula um contato adicional (não-primário) ao card. Requer permissão de update no card.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
contact_id | integer | Sim | ID do contato a vincular (resolvido dentro da conta autenticada) |
role | string | Nao | Rótulo livre do papel do contato no deal (ex: "decisor", "influenciador") |
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/5/contacts" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "contact_id": 27, "role": "decisor" }'{
"data": {
"id": 3,
"contact_id": 27,
"name": "Joao Comprador",
"email": "joao@empresa.com",
"phone_number": "+5511999999999",
"avatar_url": "",
"role": "decisor"
}
}Erros
404 se o card ou o contato não existir na conta; 422 se o contato já estiver vinculado a este card (seja como contato primário ou como adicional já existente). Entradas inválidas nunca retornam 500.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/contacts/{id}Remove um vínculo de contato adicional. Não afeta o contato primário (contact_id) do card.
O {id} é o ID do vínculo
O {id} na rota é o id do registro de vínculo (o campo id de um item de additional_contacts), não o contact_id. Retorna 204 No Content em sucesso; 404 se o vínculo não existir neste card.
curl -X DELETE "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/5/contacts/3" \
-H "api_access_token: YOUR_TOKEN"Conversas adicionais
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/conversationsVincula uma conversa adicional (não-primária) ao card. Requer permissão de update no card.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
conversation_display_id | integer | Sim | Display ID da conversa a vincular (per-account, não ID global) |
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/5/conversations" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "conversation_display_id": 57 }'{
"data": {
"id": 8,
"conversation_display_id": 57
}
}Erros
404 se o card ou a conversa não existir na conta; 422 se a conversa já estiver vinculada a este card (seja como conversa primária ou como adicional já existente). Entradas inválidas nunca retornam 500.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/conversations/{id}Remove um vínculo de conversa adicional. Não afeta a conversa primária (conversation_display_id) do card.
O {id} é o ID do vínculo
O {id} na rota é o id do registro de vínculo (o campo id de um item de additional_conversations), não o conversation_display_id. Retorna 204 No Content em sucesso; 404 se o vínculo não existir neste card.
curl -X DELETE "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/5/conversations/8" \
-H "api_access_token: YOUR_TOKEN"Mover Card entre Estágios
/api/v1/accounts/{account_id}/pipeline_cards/{id}/move_to_stageMove um card para outro estágio do pipeline.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
pipeline_stage | string | Sim | Identificador do estágio destino no formato "{pipeline_id}_{stage_slug}" (ex: "2_qualificao"). Use GET /pipelines/{id}/stages para obter os identificadores disponíveis. |
lost_reason | string | Nao | Motivo da perda quando o destino é um estágio terminal de perda |
won_value | number | Nao | Valor fechado quando o destino é um estágio terminal de ganho |
won_note | string | Nao | Nota do ganho quando o destino é um estágio terminal de ganho |
Esta rota lida com etapas terminais
Diferente do PATCH /pipeline_cards/{id}, este endpoint aceita um destino de Ganho ou Perda: ele identifica o tipo da etapa e executa o fechamento completo internamente (o mesmo caminho de mark_won/mark_lost), aproveitando won_value, won_note e lost_reason quando enviados. Nada mudou aqui — é o caminho recomendado para mover cards por API, inclusive para fechar um deal em uma única chamada.
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline_cards/5/move_to_stage" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "pipeline_stage": "1_qualificacao" }'
# Destino terminal: o fechamento acontece dentro da propria rota.
# Pegue o identificador real da etapa de Ganho em GET /pipelines/1/stages
# (o estagio com is_won_stage = true).
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline_cards/5/move_to_stage" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "pipeline_stage": "<id da etapa de Ganho>", "won_value": 18000 }'/api/v1/accounts/{account_id}/pipeline_cards/reorderReordena cards dentro de um estágio.
pipeline_id define um único escopo
Envie pipeline_id no nível raiz, que é o formato canônico. Se ele for omitido, o backend aceita apenas o fallback legado em positions[0].pipeline_id; sem nenhum dos dois, a API retorna 400. O valor escolhido vira o escopo global do reorder: cards descartados e cards de outros pipelines são ignorados, e valores de pipeline_id nas demais entradas não são processados como escopos independentes.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
pipeline_id | integer | Nao | Campo raiz canônico e recomendado. Define o único pipeline cujos cards serão reordenados. |
positions | array | Sim | Array de objetos { id, position, pipeline_stage }. Entradas sem pipeline_stage são ignoradas. |
positions[0].pipeline_id | integer | Nao | Fallback legado, consultado somente quando pipeline_id raiz está ausente. O valor passa a escopar todo o array. |
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline_cards/reorder" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline_id": 1,
"positions": [
{ "id": 5, "position": 0, "pipeline_stage": "1_qualificacao" },
{ "id": 9, "position": 1, "pipeline_stage": "1_qualificacao" }
]
}'Status do Deal
/api/v1/accounts/{account_id}/pipeline/cards/{id}/deal_status/mark_wonMarca o deal como ganho. Requer que o pipeline tenha etapas Ganho/Perdido habilitadas.
Body (opcional)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
won_value | number | Nao | Valor final fechado (numérico). Valor inválido retorna 422. |
won_note | string | Nao | Nota sobre o ganho |
winning_offer_index | integer | Nao | Índice da oferta vencedora (quando o card tem ofertas em item_details) |
pipeline_product_id | integer | Nao | Produto do catálogo que foi vendido. É esse vínculo que permite medir receita por produto. |
quantity | number | Nao | Quantidade vendida. Só faz sentido junto de pipeline_product_id. |
unit_value | number | Nao | Preço efetivamente cobrado por unidade — o preço do catálogo é apenas o ponto de partida. |
items | array | Nao | Vender vários produtos na mesma negociação (1 a 50 itens). Mutuamente exclusivo com pipeline_product_id/quantity/unit_value acima — use um formato ou outro, não os dois. |
Vendendo mais de um produto no mesmo fechamento
Envie items — um array de objetos { pipeline_product_id, quantity, unit_value, total_value, title, note, custom_attributes } — em vez dos campos escalares acima, para registrar vários produtos na mesma venda, cada um com sua própria quantidade e valor. quantity tem padrão 1 (até 3 casas decimais, deve ser positiva); total_value é obrigatório quando unit_value está ausente, e quando os dois vêm juntos total_value precisa ser igual a quantity * unit_value. pipeline_product_id por item segue a mesma recusa por escopo/inatividade/indisponibilidade descrita abaixo.
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/5/deal_status/mark_won" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"won_value": 15000,
"won_note": "Fechado no plano anual",
"pipeline_product_id": 7,
"quantity": 2,
"unit_value": 7500
}'Resposta uniforme
mark_won, mark_lost e reopen retornam o mesmo shape: { id, status, pipeline_stage, won_at, won_value, won_note, lost_at, lost_reason, item_details }. Card já ganho retorna 409.
pipeline_product_id e unit_value vêm NULL com vários itens
O bloco opportunityda resposta (ver "Razão de Vendas do Card" abaixo) sempre traz um array items com cada linha vendida. Os campos soltos pipeline_product_id e unit_value no Nível RAIZ do opportunity são apenas um eco de compatibilidade com o formato de produto único: eles vêm preenchidos SOMENTE quando a venda tem EXATAMENTE UM item, e vêm null quando a venda tem 0 itens (valor livre, sem produto) ou 2 ou mais. Já quantity nesse mesmo nível NUNCA é null — é sempre a SOMA da quantidade de todos os itens. Não trate pipeline_product_id: null como "produto não vinculado" sem antes checar se items tem mais de uma linha.
O produto é recusado fora do seu escopo
pipeline_product_id retorna 422 quando o produto pertence a outra conta, está desativado, ou não está disponível no funil daquele card. A recusa é proposital: o razão de vendas é append-only, então um vínculo errado sobreviveria à correção. Sem esse campo o fechamento continua funcionando — a venda apenas fica sem produto vinculado e não entra na medição por produto.
Reabrir e ganhar de novo registra uma SEGUNDA venda
O valor de um negócio já fechado não muda em silêncio: mark_won em card ganho retorna 409. Reabrir (reopen) e ganhar novamente registra uma nova venda — o comportamento correto para recompra —, não substitui a anterior. Para corrigir um lançamento errado, estorne-o em POST .../pipeline/opportunities/{id}/void.
/api/v1/accounts/{account_id}/pipeline/cards/{id}/deal_status/mark_lostMarca o deal como perdido.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
lost_reason | string | Nao | Motivo da perda |
/api/v1/accounts/{account_id}/pipeline/cards/{id}/deal_status/reopenReabre um deal marcado como ganho ou perdido.
Timeline do Card
/api/v1/accounts/{account_id}/pipeline/cards/{id}/timelineRetorna a timeline de atividades do card.
{
"timeline": [
{
"date": "2026-01-15",
"activities": [
{
"id": 1780508401,
"type": "history_event",
"event_type": "stage_changed",
"details": {
"user": { "id": 5, "name": "Maria Santos", "avatar_url": "" },
"old_stage": "1_prospeccao",
"new_stage": "1_qualificacao"
},
"user": { "id": 5, "name": "Maria Santos", "avatar_url": "" },
"created_at": "2026-01-15T14:30:00Z",
"timestamp": "2026-01-15T14:30:00Z"
}
]
}
]
}Anexos do Card
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/attachmentsLista anexos de um card.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/attachmentsAdiciona um anexo ao card.
Body (multipart/form-data)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
attachment(body) | file | Sim | Arquivo a anexar, com no máximo 25 MB |
Tipos de arquivo aceitos
Imagens JPEG, PNG, GIF, WebP e SVG; PDF, TXT e CSV; documentos Word, Excel e PowerPoint; e arquivos ZIP. O servidor valida o tipo real do arquivo, não apenas o header enviado pelo cliente.
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/5/attachments" \
-H "api_access_token: YOUR_TOKEN" \
-F "attachment=@/path/to/proposta.pdf"/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/attachments/{id}Remove um anexo. Operação restrita a administradores.
Sequências de Atividade no Card
Inscreve um card numa sequência de atividades (cadência automatizada de etapas, definida previamente em pipeline/activity_sequences). Sequências estão incluídas em toda licença NooviChat válida; o acesso depende das permissões do usuário e da configuração operacional da conta. Um card só pode ter uma sequência ativa/pausada da mesma definição por vez (422 em duplicidade).
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/sequencesLista as sequências inscritas no card.
{
"data": [
{
"id": 14,
"status": "active",
"current_step": 1,
"total_steps": 4,
"progress_percentage": 25,
"definition": { "id": 3, "name": "Cadência de prospeccao" },
"next_step_at": "2026-06-15T12:00:00-03:00",
"steps_log": [],
"created_at": "2026-06-13T09:00:00-03:00",
"updated_at": "2026-06-13T09:00:00-03:00"
}
]
}/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/sequencesInicia uma sequência no card.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
definition_id | integer | Sim | ID de uma activity_sequence ATIVA da conta |
Respostas
201 com a sequência criada;404 se a definição não existir/estiver inativa; 422 se já houver uma sequência ativa/pausada da mesma definição no card.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/sequences/external_startPonto de entrada para integrações externas (ex: n8n). Inicia uma sequência com contexto controlado.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
definition_id | integer | Sim | ID de uma activity_sequence ativa |
context | object | Nao | Objeto de até 10 KB. Chaves aceitas: trigger_source, metadata, external_id, source_url e notes. Chaves desconhecidas são descartadas; valor não-objeto retorna 400. |
Respostas desta entrada externa
Retorna 201 quando inicia. Definição inexistente ou inativa retorna 422 nesta rota (a rota normal POST .../sequences usa 404 para o mesmo caso); duplicidade ativa/pausada retorna 422.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/sequences/{id}/pausePausa a execução ativa da sequência. 422 se não houver execução ativa.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/sequences/{id}/resumeRetoma uma sequência pausada. 422 se não houver execução pausada.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/sequences/{id}/complete_stepAvança manualmente para a próxima etapa da sequência. 422 se não houver execução ativa ou se não restarem etapas.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/sequences/{id}Cancela a sequência no card (marca como cancelled). Retorna 200.
Operações em Lote
Operação administrativa em massa — escopo de conta
bulk_assign exige administrador e opera apenas sobre cards da conta autenticada (Current.account.pipeline_cards.where(id: item_ids)). IDs de cards de outras contas são silenciosamente ignorados — não há vazamento cross-tenant. O lote é limitado a 200 cards por requisição; passar mais retorna 400. Esta operação apenas reatribui responsável — não remove nem move cards.
/api/v1/accounts/{account_id}/pipeline/cards/bulk_assignAtribui responsável para vários cards de uma vez (max 200).
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
item_ids | array | Sim | IDs dos cards (max 200, escopados à conta autenticada) |
owner_id | integer | Nao | ID do novo responsável. Se distribution não for usado, omitir owner_id desatribui os cards. |
distribution | string | Nao | round_robin ou workload_balanced — distribui entre agentes/administradores com disponibilidade online ou busy. Se omitido, usa owner_id. |
Omitir owner_id e distribution desatribui
Para distribuição automática, envie explicitamente distribution. Se os dois campos forem omitidos, o backend atualiza os cards com responsável nulo.
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/cards/bulk_assign" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "item_ids": [5, 9, 12], "owner_id": 3 }'/api/v1/accounts/{account_id}/pipeline/cards/{id}/assignAtribui um responsável a um card específico.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
owner_id | integer | Nao | ID do responsável. Omita para desatribuir o card. |
notify | boolean | Nao | Enviar notificação ao novo responsável |
Automações do Pipeline
Crie workflows automatizados que executam ações quando cards mudam de estágio, atingem determinado score ou atendem condições específicas.
Alterações de automação são administrativas
Criar, atualizar, remover, importar, duplicar ou executar automações exige uma conta de administrador. Usuários autenticados podem consultar as automações dos pipelines que conseguem visualizar.
/api/v1/accounts/{account_id}/pipeline/automationsLista as automações globais e as dos pipelines visíveis ao usuário. Retorna um array (cada automação inclui stats agregado).
[
{
"id": 1,
"name": "Notificar ao entrar em Negociação",
"description": "Avisa o responsável quando um card chega na negociação",
"active": true,
"trigger_type": "event",
"pipeline_id": 1,
"trigger": { "event": "stage_change", "to_stage": "1_negociacao" },
"conditions": [],
"actions": [{ "type": "send_notification", "to": "owner" }],
"flow": { "nodes": [], "connections": [], "viewport": { "x": 0, "y": 0, "zoom": 1 } },
"stats": {
"total_executions": 23,
"success_count": 22,
"failure_count": 1,
"success_rate": 96
}
}
]/api/v1/accounts/{account_id}/pipeline/automationsCria uma nova automação. Body envolto em pipeline_automation.
Body envolto em pipeline_automation
Os campos devem estar dentro do wrapper pipeline_automation. trigger_type aceita: event, webhook, scheduled ou manual.
Body (pipeline_automation)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
name | string | Sim | Nome da automação |
description | string | Nao | Descrição |
active | boolean | Nao | Se a automação está ativa |
trigger_type | string | Nao | event | webhook | scheduled | manual |
pipeline_id | integer | Nao | ID do pipeline alvo |
trigger | object | Nao | Configuração do gatilho legado (ex: { event, to_stage }). Complementar: não substitui o flow. |
conditions | array | Nao | Array de condições para acionar |
actions | array | Nao | Ações legadas. Complementares: não substituem o flow. |
schedule_config | object | Nao | Configuração de agendamento (quando trigger_type = scheduled) |
flow | object | Sim | Editor visual: { nodes, connections, viewport }. nodes precisa ser não vazio — é o único formato que o motor executa. |
Sem fluxo, sem automação
O motor só executa automações baseadas em fluxo. Criar uma sem flow.nodes preenchido retorna 422 — antes o registro era salvo e aparecia ativo na lista, mas nenhum evento o disparava.
O formato legado (trigger + actions sem flow) não é mais aceito na criação. Monte o fluxo visual — POST .../automations/validate_flow valida o grafo antes de salvar.
{
"error": "An automation without a flow is never executed by the engine. Build the flow before saving.",
"valid": false,
"errors": [
"An automation without a flow is never executed by the engine. Build the flow before saving."
]
}curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/automations" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline_automation": {
"name": "Notificar em Negociacao",
"active": true,
"trigger_type": "event",
"pipeline_id": 1,
"flow": {
"nodes": [
{
"id": "trigger-1",
"type": "trigger",
"position": { "x": 0, "y": 0 },
"data": {
"type": "item_moved",
"event_type": "pipeline_item_moved",
"pipeline_id": "1",
"from_stage_id": "lead",
"to_stage_id": "negociacao",
"conditions": [
{ "field": "conversation.label_list", "operator": "equals", "value": "vip" }
]
}
},
{
"id": "action-1",
"type": "action",
"position": { "x": 360, "y": 0 },
"data": { "type": "send_message", "content": "O card entrou em negociacao" }
}
],
"connections": [
{ "id": "edge-1", "source": "trigger-1", "target": "action-1" }
],
"viewport": { "x": 0, "y": 0, "zoom": 1 }
}
}
}'/api/v1/accounts/{account_id}/pipeline/automations/{id}Atualiza uma automação.
Duas edições recusadas com 422
A mesma regra do POST vale aqui, mas avaliada sobre o estado resultante — o PATCH só é recusado quando é ele que deixa a automação ativa e inexecutável:
- Esvaziar o fluxo (
flow.nodesvazio) de uma automação ativa. - Ativar (
active: true) uma automação que não tem fluxo — tipicamente uma legada, anterior a esta regra.
Automações legadas que já estão ativas continuam editáveis e, principalmente, continuam podendo ser desativadas: o objetivo é tirar a conta desse estado, não prendê-la nele. Enviar um flow com nós inválidos retorna 422 pelo validador de fluxo, com a lista de erros em errors.
/api/v1/accounts/{account_id}/pipeline/automations/{id}Remove uma automação.
/api/v1/accounts/{account_id}/pipeline/automations/{id}/duplicateClona uma automação existente.
Contrato canônico do fluxo visual
O objeto flow usa nodes, connections e, opcionalmente, viewport. Cada node possui id, type, dados em data e pode incluir position. O objeto node.data é extensível: o backend preserva chaves e tipos JSON necessários para cada ação, remove tags HTML de strings e valida a estrutura executável. Por isso, a API não define um schema fechado de parâmetros para todas as ações.
Use nomes canônicos em novas integrações
Create, update e import normalizam o fluxo antes de gravar. IDs numéricos aceitos nos filtros são persistidos como strings; arrays de IDs aceitam apenas inteiros positivos. Clientes novos devem enviar somente as chaves canônicas abaixo.
Filtros canônicos de trigger em node.data
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
event_type | string | Nao | Evento despachado; para movimento de card use pipeline_item_moved |
pipeline_id | string | Nao | ID positivo do pipeline, normalizado para string |
from_stage_id / to_stage_id | string | Nao | Chaves dos estágios de origem e destino |
pattern | string | Nao | Padrão textual do gatilho |
inbox_ids / added_label_ids | string[] | Nao | Arrays de IDs positivos, sem duplicatas após normalização |
stage_ids | string[] | Nao | Lista de chaves de estágio |
channel_type | string | Nao | Tipo canônico do canal |
message_type | string | Nao | all | incoming | outgoing |
only_first_message | boolean | Nao | Aceita somente boolean real, não "true"/"false" em string |
from_status / to_status | string | Nao | Status anterior e novo |
label_action | string | Nao | any | added | removed |
filter_label_id | string | Nao | ID positivo da label, normalizado para string |
added_labels / removed_labels | string[] | Nao | Nomes de labels; cada item deve ser string não vazia |
Campos canônicos em condições
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
contact.phone_number | string | Nao | Telefone do contato |
conversation.assignee_id | string | Nao | ID do responsável da conversa |
conversation.team_id | string | Nao | ID do time da conversa |
inbox.channel_type | string | Nao | Classe do canal da inbox |
conversation.label_list | string[] | Nao | Lista real de labels da conversa; equals/not_equals com valor escalar testa pertinência sem diferenciar maiúsculas |
Aliases legados existem apenas para compatibilidade
O backend ainda lê e normaliza os aliases pipeline_card_stage_changed, eventType, pipelineId, funnel_id, fromColumn, stage_from, toColumn, stage_to, keyword, inboxIds, channelType, messageType, onlyFirstMessage, stageIds, fromStatus, toStatus, labelAction, filterLabelId, addedLabelIds, addedLabels e removedLabels. Condições antigas com contact.phone, conversation.assignee, conversation.team, conversation.channel ou conversation.label também são convertidas. Eles não devem ser gravados por clientes novos.
Aliases equivalentes com o mesmo valor são consolidados. Valores conflitantes ou tipos inválidos retornam 422; create, update e import usam a mesma fronteira de normalização canônica.
/api/v1/accounts/{account_id}/pipeline/automations/validate_flowValida um flow sem salvar. Envie { flow: { nodes, connections, viewport } }.
{ "valid": true, "errors": [], "warnings": [] }{
"valid": false,
"errors": ["Node trigger-1: conflicting values for pipeline_id, pipelineId"],
"warnings": []
}/api/v1/accounts/{account_id}/pipeline/automations/importImporta uma automação JSON, normaliza o flow e sempre cria o registro inativo.
Resposta de erro do import
Para o mesmo conflito do exemplo acima, o import retorna exatamente { "error": "Flow Node trigger-1: conflicting values for pipeline_id, pipelineId" } com status 422.
Executar e Testar
Execução e simulação são administrativas
execute e dry_run exigem administrador. Ambos aceitam, no nível raiz, conversation_id (ID interno da conversa, não o display ID) oucontact_id. Se os dois forem enviados, a conversa tem precedência.
/api/v1/accounts/{account_id}/pipeline/automations/{id}/executeExecuta manualmente uma automação.
Body (opcional)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
conversation_id | integer | Nao | ID interno da conversa usada como contexto |
contact_id | integer | Nao | ID do contato usado como contexto quando conversation_id não é enviado |
/api/v1/accounts/{account_id}/pipeline/automations/{id}/dry_runExecuta simulação sem efeito real (teste).
Body (opcional)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
conversation_id | integer | Nao | ID interno da conversa usada como contexto |
contact_id | integer | Nao | ID do contato usado como contexto quando conversation_id não é enviado |
Dry Run
Permite testar sem executar ações reais. Sucesso retorna 200 com success, dry_run, dados da automação, steps, log e summary. Erro de validação/simulação retorna o objeto direto com success: false e status 422.
/api/v1/accounts/{account_id}/pipeline/automations/{id}/validateValida a configuração da automação.
/api/v1/accounts/{account_id}/pipeline/automations/{id}/executionsHistórico de execuções da automação.
Estatísticas de Automação
/api/v1/accounts/{account_id}/pipeline/automations/{id}/statsMétricas de uma automação específica.
/api/v1/accounts/{account_id}/pipeline/automations/dashboardDashboard de todas as automações.
/api/v1/accounts/{account_id}/pipeline/automations/{id}/audit_logsLogs de auditoria da automação.
Catálogo de Produtos
O que a conta vende. Cadastrado aqui, o produto fica selecionável ao fechar um card como ganho — e é esse vínculo que permite medir receita por produto. Ler o catálogo acompanha o acesso ao Pipeline; criar, editar e desativar exigem permissão de gestão.
Depende do módulo Oportunidades
Catálogo e razão de vendas fazem parte de Oportunidades, ativado por conta. Sem ele, estes endpoints respondem 403 com code: pipeline_opportunities_disabled — e o funil continua marcando ganho e perdido normalmente, com o valor no próprio card.
/api/v1/accounts/{account_id}/pipeline/productsLista o catálogo, ordenado por posição e nome.
Query
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
active_only | boolean | Nao | Traz somente os produtos ativos |
pipeline_id | integer | Nao | Somente os disponíveis naquele funil (inclui os irrestritos) |
per_page | integer | Nao | Padrão 50, máximo 200 |
offset | integer | Nao | Paginação |
pipeline_ids vazio significa TODOS os funis
O produto sem restrição vale em qualquer funil. O campo pipeline_ids_malformed: truesinaliza uma restrição corrompida: nesse caso o servidor trata o produto como indisponível em todo lugar (fail-closed), e você não deve ler a lista vazia como "todos os funis".
/api/v1/accounts/{account_id}/pipeline/productsCria um produto no catálogo.
Body (pipeline_product)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
name | string | Sim | Nome do produto ou serviço |
sku | string | Nao | Código único na conta. Duplicado retorna 422. |
description | string | Nao | Descrição livre |
category | string | Nao | Categoria para agrupar no catálogo |
default_value | number | Nao | Preço de tabela. Serve de ponto de partida ao fechar — o valor cobrado pode ser outro. |
currency | string | Nao | ISO-4217. Omitido, herda a moeda da conta (não o padrão BRL da coluna). |
active | boolean | Nao | Padrão true |
position | integer | Nao | Ordem no catálogo |
pipeline_ids | array | Nao | Restringe a esses funis. Vazio ou ausente = disponível em todos. |
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/products" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "pipeline_product": { "name": "Consultoria de Implantacao", "sku": "CONS-IMPL", "default_value": 3000 } }'pipeline_ids precisa ser um array de ids
Qualquer outro formato retorna 422 em vez de ser descartado em silêncio: uma restrição malformada viraria um produto disponível em todos os funis — o oposto da intenção.
/api/v1/accounts/{account_id}/pipeline/products/{id}Atualiza um produto. Mesmos campos do POST.
/api/v1/accounts/{account_id}/pipeline/products/{id}Desativa o produto (active: false). Não apaga.
Produto não é apagado, é desativado
O histórico financeiro aponta para ele, e o banco recusa o DELETE justamente para não deixar receita órfã. Desativado, o produto some da seleção de venda mas continua nomeando as vendas passadas. Para reativar, use PATCH com active: true.
/api/v1/accounts/{account_id}/pipeline/products/performanceQuanto cada produto vendeu num período.
Query
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
won_start | string (YYYY-MM-DD) | Nao | Primeiro dia do intervalo. Omitido = desde sempre. |
won_end | string (YYYY-MM-DD) | Nao | Último dia, incluído por inteiro. Anterior a won_start retorna 422. |
pipeline_id | integer | Nao | Restringe a medição a um funil |
curl "https://chat.seudominio.com/api/v1/accounts/1/pipeline/products/performance?won_start=2026-07-01&won_end=2026-07-31" \
-H "api_access_token: YOUR_TOKEN"Como ler a resposta
Todos os produtos voltam, inclusive os que não venderam (com performance: []) — um produto que não vende é o dado mais acionável da lista. performance traz uma entrada por moeda: somar BRL com USD produziria um número que não existe. Vendas estornadas ficam de fora.
O bloco unlinked, à parte, conta as vendas sem produto vinculado — as anteriores ao catálogo e as fechadas com valor livre. Sem ele a lista mostraria zeros sem explicação.
O intervalo é resolvido no fuso da conta e meta.time_zone informa qual fuso foi aplicado. Datas com hora (ou qualquer formato fora de YYYY-MM-DD) retornam 422: elas carregariam um deslocamento próprio e mediriam um recorte diferente do pedido.
Agentes veem apenas os próprios funis
Para um agente, a soma cobre somente os funis de que ele participa — inclusive no bloco unlinked. Administradores recebem a conta inteira.
Razão de Vendas do Card
Cada venda de um card é um lançamento. O razão é append-only: correção se faz por estorno, nunca apagando a linha — e por isso o histórico consegue distinguir uma correção de um sumiço.
Depende do módulo Oportunidades
Sem Oportunidades ativado na conta, estes endpoints respondem 403 com code: pipeline_opportunities_disabled. Fechar um card como ganho continua funcionando: o card guarda a data e o valor, mas a resposta de mark_won não traz o bloco opportunity e pipeline_product_id é ignorado.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/opportunitiesLista as vendas do card em ordem cronológica, com o resumo do acumulado.
/api/v1/accounts/{account_id}/pipeline/cards/{card_id}/opportunitiesRegistra uma venda avulsa no card (o caminho normal é fechar o card como ganho).
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
total_value | number | Nao | Valor total. Tem precedência sobre quantity x unit_value. |
quantity | number | Nao | Quantidade vendida (padrão 1) |
unit_value | number | Nao | Valor unitário |
pipeline_product_id | integer | Nao | Produto vendido. Id de outra conta retorna 404. |
title | string | Nao | Descrição do lançamento. Omitido, herda o nome do produto e depois o título do card. |
note | string | Nao | Observação |
items | array | Nao | Vender vários produtos na mesma venda (1 a 50 itens). Mutuamente exclusivo com total_value/quantity/unit_value/pipeline_product_id acima. |
Venda avulsa acontece AGORA
O lançamento é datado no momento da chamada, não no fechamento do card. Herdar a data do ganho retrodataria receita nova para um período já fechado.
items segue o mesmo formato do mark_won
Cada item é um objeto { pipeline_product_id, quantity, unit_value, total_value, title, note, custom_attributes }. Mesmas regras: quantity padrão 1, total_value obrigatório sem unit_value e deve bater com quantity * unit_value quando os dois vêm juntos. A resposta traz o array items completo, e os campos soltos pipeline_product_id/unit_value no nível raiz do opportunitysó vêm preenchidos com exatamente um item — veja o aviso na seção "Status do Deal" acima, que vale igual aqui.
/api/v1/accounts/{account_id}/pipeline/opportunities/{id}/voidEstorna uma venda: ela deixa de contar na receita, mas continua visível no histórico.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
reason | string | Sim | Motivo do estorno. Obrigatório, de 1 a 255 caracteres. |
Estorno não apaga
A linha permanece, marcada, com autor, data e motivo — e o acumulado do card é recalculado. Estornar duas vezes retorna 409.
Quem pode estornar
Somente administrador ou agente cuja função personalizada tenha a permissão pipeline_manage. Qualquer outro agente recebe 403.
reason é obrigatório
POST .../void sem reason, com reason vazio ou com mais de 255 caracteres retorna 422. Se a sua integração ainda não envia esse campo, atualize-a antes de estornar — não há fallback silencioso.
Webhooks do Pipeline
Receba eventos do Pipeline em tempo real numa URL externa. Gerenciar webhooks requer permissão de administrador.
A URL precisa ser pública
A URL do webhook deve resolver para um IP público. Endereços privados/internos (127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16, IPv6 ULA/link-local) são bloqueados por proteção anti-SSRF e a criação retorna 422 — a verificação também roda no momento do envio.
/api/v1/accounts/{account_id}/pipeline/webhooksCria um webhook. Body envolto em pipeline_webhook.
Body (pipeline_webhook)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
name | string | Sim | Nome do webhook |
url | string | Sim | URL HTTP ou HTTPS pública que receberá os POSTs. Em produção, prefira HTTPS. |
pipeline_id | integer | Nao | Restringe o webhook a um pipeline da conta. Omitido/null cria um webhook global. |
active | boolean | Nao | Ativa a entrega; padrão true |
events | string[] | Sim | Lista não vazia de eventos disponíveis (ver abaixo). Evento inválido retorna 422. |
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/pipeline/webhooks" \
-H "api_access_token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline_webhook": {
"name": "Integracao CRM",
"url": "https://example.com/hooks/pipeline",
"events": ["pipeline_card_won", "pipeline_card_lost"]
}
}'{ "id": 71, "name": "Integracao CRM", "url": "https://example.com/hooks/pipeline", "pipeline_id": null, "pipeline_name": null, "events": ["pipeline_card_won", "pipeline_card_lost"], "active": true, "secret": "<64 hex>" }/api/v1/accounts/{account_id}/pipeline/webhooksLista os webhooks (admin).
/api/v1/accounts/{account_id}/pipeline/webhooks/{id}Detalhes de um webhook.
/api/v1/accounts/{account_id}/pipeline/webhooks/{id}Atualiza name, url, pipeline_id, events ou active.
/api/v1/accounts/{account_id}/pipeline/webhooks/{id}Remove o webhook.
/api/v1/accounts/{account_id}/pipeline/webhooks/{id}/testDispara um POST de teste síncrono para a URL configurada.
/api/v1/accounts/{account_id}/pipeline/webhooks/{id}/regenerate_secretGera um novo secret HMAC (invalida o anterior).
Formatos de resposta
A listagem retorna { payload: [...] }. Criação (201), detalhe e atualização (200) retornam o objeto diretamente, incluindopipeline_id, pipeline_name, active, secret e os dados da última entrega. O DELETE retorna 204 No Content.
Eventos disponíveis
Oito eventos:
pipeline_card_created— card criadopipeline_card_updated— card atualizadopipeline_card_deleted— card removido (soft delete)pipeline_card_stage_changed— mudou de estágio (incluifrom_stage/to_stage)pipeline_card_won— marcado como ganho;won_atfica emdata.cardepre_won_stageemdata.card.item_detailspipeline_card_lost— marcado como perdido;lost_reasonfica emdata.cardepre_lost_stageemdata.card.item_detailspipeline_card_owner_changed— responsável alteradopipeline_card_sla_exceeded— o card passou do prazo na etapa. Único evento que nasce de um relógio, não de uma ação: é justamente o que acontece sem ninguém olhando. Além do card, o payload trazsla_hours(o prazo da etapa),seconds_in_stage(o tempo já gasto) estage— só "estourou" não distingue um minuto de atraso de uma semana parada.
O SLA é verificado de hora em hora, e avisa uma vez por etapa
A varredura roda a cada hora, então o evento chega depois do estouro, não no instante dele. E ele é enviado uma vez por etapa: mover o card e trazê-lo de volta rearma o aviso, ficar parado não repete. Configure o prazo em stages[].sla_hours do funil e habilite stage_sla_enabled.
Formato do payload
{
"event": "pipeline_card_won",
"timestamp": "2026-06-03T23:21:38Z",
"data": {
"card": {
"id": 12473,
"pipeline_id": 8143,
"pipeline_stage": "8143_ganho",
"won_at": "...",
"contact_id": 21,
"item_details": { "pre_won_stage": "8143_negociacao" }
}
}
}Verificação da assinatura (HMAC)
Cada POST inclui o header X-Pipeline-Webhook-Signature: sha256=<hex>, onde <hex> é o HMAC-SHA256 do corpo JSON cru usando o secret do webhook (64 hex). Recalcule e compare para garantir a autenticidade:
import crypto from "crypto";
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-pipeline-webhook-signature"]));