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

A maior parte das integrações de Pipeline que dão errado em produção falha no mesmo ponto: tratam concorrência como erro de transporte e repetem a chamada. Repetir cegamente um movimento de funil desfaz o trabalho de um atendente sem deixar rastro.

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:

bash
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

Trocar o número na URL não concede acesso. Um bot só enxerga a conta em que foi criado. Se a sua integração precisa atender várias contas, ela precisa de um bot por conta.

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

Uma allowlist estreita é a forma recomendada de colocar um agente para organizar o funil sem autoridade para fechar negócio: conceda 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

403 é você não pode. 404 é isso não está no seu alcance. Se o seu agente recebe 404 num card que você sabe que existe, o problema é a ACL de funis, não o identificador.

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.

bash
GET /api/v1/accounts/{account_id}/agent_bots/me
json
{
  "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

Se 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.

bash
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.

bash
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

Um bot que omite o campo recebe 422 em todo movimento, deterministicamente — não é intermitente e não melhora com retry. Chamadas autenticadas como pessoa podem omitir e mantêm o comportamento antigo, para não derrubar integrações existentes.

7. Matriz de respostas

StatusSignificaO que fazer
200Movido. O corpo traz o novo stage_version.Seguir.
401Token ausente, inválido ou revogado.Corrigir credencial. Retry não resolve.
403Permissão ou pipeline_actions não concedem a ação.Corrigir configuração do bot. Retry não resolve.
404Card fora do alcance da ACL, ou inexistente.Revisar ACL de funis. Retry não resolve.
409Concorrê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.
422Contrato 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

São diagnósticos opostos. 422 é integrador desatualizado ou mal configurado: alguém precisa mudar código ou permissão. 409 é o sistema funcionando: um humano mexeu no card. Um cliente que grava os dois como 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:

reasonStatusO que aconteceuReação correta
expected_version_required422Agent bot não enviou expected_version.Corrigir o cliente. GET novo antes de tentar de novo.
invalid_expected_version422O 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_conflict409Versão válida, mas o card mudou antes da sua escrita.Reler e reavaliar a decisão — seção 8.
action_not_allowed_for_bot403O 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.
json
// 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

O 422 não devolve 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.

python
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

Se depois de N rodadas o card continua mudando, isso normalmente quer dizer que uma pessoa está trabalhando nele agora. Registrar e sair é melhor comportamento que insistir.

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

Um funil pode ter uma etapa chamada 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_view e, se houver allowlist, cards_move.
  • Ganhar / perder exige pipeline_manage e, 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} e cards_edit. Edição não mexe em stage_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_actions nã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:

  1. Contrato. O simulador devolve 422 quando expected_version não vem. Se o seu teste continua verde, ele não está testando o contrato real.
  2. 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.
  3. 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

Vale forçar o conflito de propósito no ambiente de homologação. É a única forma de saber se o seu agente reavalia ou se ele repete.