Agente externo operando o Pipeline
Este é o contrato para um agente autônomo (um bot de IA, um worker, uma automação) que move cards de funil pela API do NooviChat. Ele existe porque a parte difícil não é chamar o endpoint: é conviver com um humano que arrasta o mesmo card no painel enquanto o seu agente pensa.
Leia a seção 8 antes de escrever código
O agente é um principal de autenticação
Um AgentBot não é uma alternativa à API: é o principal com que a sua automação se autentica nela. Isso lhe dá três coisas que um token de pessoa não dá: identidade técnica própria na auditoria, uma allowlist de ações que você controla, e a possibilidade de revogar o agente sem desligar ninguém do time.
1. Criar e vincular o AgentBot
No painel, em Configurações → Bots de Agente, crie o bot e copie o token. O token é exibido uma vez. Depois vincule o bot às caixas de entrada em que ele deve atuar.
Toda chamada usa o cabeçalho api_access_token:
curl https://SEU-DOMINIO/api/v1/accounts/1/agent_bots/me \
-H "api_access_token: SEU_TOKEN_DE_BOT"O account_id da URL é contexto, não autoridade
2. As três camadas de autorização
Um pedido do bot passa por três filtros independentes. Ele precisa sobreviver aos três, e cada um recusa de um jeito diferente:
- Permissão geral — o que o bot pode fazer no produto (
pipeline_view,pipeline_manage). Recusa com 403. - ACL de funis — quais funis esse bot alcança. Um card num funil fora do alcance não retorna 403: retorna 404, porque para esse bot ele não existe.
pipeline_actions— a allowlist fina dentro do que a permissão já concede:cards_move,cards_win,cards_lose,cards_edit,cards_assign,pipelines_manage. Recusa com 403.
403 de allowlist é o desenho funcionando
cards_move e não conceda cards_win / cards_lose. O agente lê tudo, move entre etapas comuns, e recebe 403 ao tentar fechar — com allowed_actions dizendo exatamente o que ele pode. Isso é impossível com token de pessoa, porque pipeline_actions não existe para usuário.403 e 404 querem dizer coisas diferentes
3. O que lista vazia significa
Esta é a parte que mais surpreende integrador: pipeline_actions: [] não quer dizer nenhuma ação permitida. Quer dizer sem restrição adicional — o bot pode tudo que a permissão geral dele já concede.
Você não precisa deduzir isso. A própria API resolve a ambiguidade em uma palavra, no campo pipeline_actions_effect:
unrestricted— a lista está vazia e não restringe nada.narrowed— a lista tem itens e é o limite efetivo.
4. Descobrir o que o bot pode fazer
Não codifique as capacidades do agente em configuração paralela: pergunte para o servidor. O endpoint de descoberta responde tudo o que a seção 2 descreve, já com a ACL aplicada.
GET /api/v1/accounts/{account_id}/agent_bots/me{
"id": 118,
"name": "Meu Agente",
"pipeline_actions": [],
"pipeline_actions_effect": "unrestricted",
"pipelines": [
{ "id": 15236, "name": "Comercial", "stages": ["entrada_de_lead", "..."] }
],
"endpoints": ["pipeline_cards", "conversations", "..."]
}Faça disso um teste de fumaça na sua subida
pipelines vier vazio, o agente está autenticado e sem alcance nenhum. É melhor descobrir isso no deploy do que no primeiro card que ele tentar mover.5. stage_version: o campo canônico
Todo card carrega stage_version: um contador que sobe a cada movimentação de etapa. Ele é a base do controle de concorrência. Não derive versão de updated_at, de hash do payload nem da etapa atual — só stage_version tem essa garantia.
GET /api/v1/accounts/{account_id}/pipeline_cards/{id}6. Mover um card
O movimento é um compare-and-swap: você declara a versão sobre a qual decidiu, e o servidor só aplica se ela ainda for a atual. A comparação e a escrita acontecem na mesma transação.
POST /api/v1/accounts/{account_id}/pipeline_cards/{id}/move_to_stage
{
"pipeline_stage": "15236_contato_realizado",
"expected_version": 4
}Para agent bots, expected_version é obrigatório
7. Matriz de respostas
| Status | Significa | O que fazer |
|---|---|---|
| 200 | Movido. O corpo traz o novo stage_version. | Seguir. |
| 401 | Token ausente, inválido ou revogado. | Corrigir credencial. Retry não resolve. |
| 403 | Permissão ou pipeline_actions não concedem a ação. | Corrigir configuração do bot. Retry não resolve. |
| 404 | Card fora do alcance da ACL, ou inexistente. | Revisar ACL de funis. Retry não resolve. |
| 409 | Concorrência real: alguém mexeu no card entre a sua leitura e a sua escrita. O card não foi movido. | Reler e reavaliar — ver seção 8. |
| 422 | Contrato inválido: expected_version omitido, mal formatado, ou etapa inexistente. O pedido nem chegou a ser avaliado. | Corrigir o cliente. Retry não resolve. |
Não colapse 409 e 422 num erro genérico
write_rejected perde a única informação que diria qual dos dois está acontecendo — e some com o sinal justamente quando ele importa.Sempre use o campo reason para distinguir, nunca o texto de error, que pode mudar. Estes são os valores literais que o endpoint devolve:
reason | Status | O que aconteceu | Reação correta |
|---|---|---|---|
expected_version_required | 422 | Agent bot não enviou expected_version. | Corrigir o cliente. GET novo antes de tentar de novo. |
invalid_expected_version | 422 | O valor não é um inteiro não-negativo — string, negativo, nulo, objeto. Envie um escalar. | Corrigir o cliente. GET novo antes de tentar de novo. |
stage_version_conflict | 409 | Versão válida, mas o card mudou antes da sua escrita. | Reler e reavaliar a decisão — seção 8. |
action_not_allowed_for_bot | 403 | O bot está autenticado, mas a ação não está na allowlist dele. A resposta traz allowed_actions com o que ele pode fazer. | Não reautentique — o token é válido. Pule a ação e siga. |
Os dois 422 não são o mesmo problema
expected_version_required quer dizer que o cliente não implementa o contrato. invalid_expected_version quer dizer que implementa, mas está mandando o valor errado — tipicamente o objeto inteiro do card em vez do escalar stage_version, ou uma string. Colapsar os dois num erro genérico esconde exatamente a informação que diz qual dos dois consertar.// 422 — o pedido nunca foi avaliado
{
"error": "expected_version is required for agent bots on this endpoint",
"reason": "expected_version_required",
"requires_fresh_read": true
}
// 422 — valor mal formado (string, negativo, objeto)
{
"error": "expected_version must be a non-negative integer",
"reason": "invalid_expected_version",
"requires_fresh_read": true
}
// 409 — houve corrida de verdade
{
"error": "Pipeline card stage changed since the version you sent",
"reason": "stage_version_conflict",
"expected_version": 4,
"current_version": 6,
"current_stage": "15236_proposta"
}Repare no que o 422 NÃO traz
current_version, de propósito, e carrega requires_fresh_read: true. O pedido foi recusado antes de qualquer comparação, então não existe ali uma versão sobre a qual você tenha decidido. Repetir com um número tirado do corpo de erro é última escrita vence com passos a mais — e é pior que o erro original, porque passa a responder 200 e nada denuncia que a decisão foi tomada sobre um estado que já não existe.8. O algoritmo obrigatório
No 409, a resposta certa não é repetir: é decidir de novo. A premissa que levou o agente a escolher o destino pode ter deixado de valer. Um card que ele ia mover para Contato realizado pode ter sido fechado como ganho por um atendente nesse intervalo.
def mover(card_id, decidir, tentativas=3):
for _ in range(tentativas):
card = GET(f"/pipeline_cards/{card_id}")
# A decisão é refeita a cada rodada, sobre o estado recém-lido.
# Isto é o ponto do algoritmo: não é retry, é reavaliação.
destino = decidir(card)
if destino is None:
return "nada a fazer" # a premissa mudou; desistir é correto
if destino == card["pipeline_stage"]:
return "ja esta la"
r = POST(f"/pipeline_cards/{card_id}/move_to_stage", {
"pipeline_stage": destino,
"expected_version": card["stage_version"],
})
if r.status == 200:
return "movido"
if r.status == 409:
continue # alguem mexeu: reler e reavaliar
raise Exception(r.body["reason"]) # 401/403/404/422: retry nao resolve
return "desistiu apos concorrencia repetida"Desistir é um resultado legítimo
9. Mover, ganhar, perder e editar
Ganhar e perder não são endpoints separados: são movimentos para uma etapa marcada como de ganho ou de perda. O que determina o desfecho é a flag da etapa (is_won_stage / is_lost_stage), não o nome dela.
Uma etapa chamada Ganho pode não ser a etapa de ganho
ganho sem a flag e outra, parecida, com a flag. Mover para a primeira retorna 200 e não fecha negócio nenhum: o card muda de coluna e won_at continua nulo. Sempre escolha a etapa pela flag que vem em agent_bots/me, nunca pelo nome.- Mover entre etapas comuns exige
pipeline_viewe, se houver allowlist,cards_move. - Ganhar / perder exige
pipeline_managee, se houver allowlist,cards_win/cards_lose. Fechar negócio é uma autoridade separada de circular no funil. - Editar campos do card usa
PATCH /pipeline_cards/{id}ecards_edit. Edição não mexe emstage_version: o contador é de etapa.
10. Razão de ciclos
A partir da v4.17.1.1, todo fechamento de card é registrado no razão de oportunidades, independentemente de qualquer flag de funcionalidade. Os registros são imutáveis: uma correção não reescreve o histórico, ela estorna o lançamento anterior e grava um novo.
Para o seu agente isso significa que fechar um card duas vezes por engano não some do histórico — fica visível como estorno. Vale escrever o integrador supondo que cada fechamento é permanente e auditável.
11. Não use token humano
É tecnicamente possível autenticar uma automação com o token pessoal de um usuário administrador, e vários integradores fazem isso por ser o caminho mais curto. Não faça.
- Autoridade demais. O token da pessoa carrega tudo que ela pode fazer no produto, não só o Pipeline.
- Auditoria mentirosa. As ações do robô aparecem com o nome de uma pessoa. Quando algo der errado, o histórico aponta para quem não fez.
- Sem allowlist.
pipeline_actionsnão existe para usuário. Você perde o controle fino da seção 2. - Frágil. A pessoa sai da empresa, troca a senha ou perde acesso, e a automação cai junto.
12. Como testar de verdade
O erro mais comum em teste de integração de Pipeline é o transporte simulado só devolver o que o cliente espera. Um mock que aceita {"pipeline_stage": "..."} e responde 200 deixa passar um cliente que nunca manda expected_version — e que vai receber 422 em cada chamada assim que subir.
Os três cenários que o seu teste precisa cobrir:
- Contrato. O simulador devolve 422 quando
expected_versionnão vem. Se o seu teste continua verde, ele não está testando o contrato real. - Corrida com humano. Leia o card, mova-o por fora (pelo painel, ou com outra chamada), e só então mande o POST com a versão antiga. Espere 409, e verifique que o seu código reavaliou em vez de reenviar.
- Desfecho. Confirme que mover para a etapa de ganho realmente preencheu
won_at. Se preencheu nulo, você moveu para uma etapa homônima sem a flag — ver seção 9.
Um teste que nunca viu um 409 não testou concorrência