GoHighLevel (GHL) API V2 - Não é possível fazer POST/PUT em Campos Personalizados do Negócio baseado no ID Externo do ERP (Erros 401 e 422)

Oi a todos,

Estou construindo um fluxo de sincronização no n8n (Cloud versão 2.22.6) para atualizar dados de vendas do nosso ERP para perfis de negócios do GoHighLevel (GHL) (Empresas).

Meu Objetivo:

Processamos um arquivo CSV contendo totais de faturas divididos por marca. Precisamos procurar a Empresa no GHL usando um ID ERP externo imutável (codice_cliente) armazenado dentro de um Campo Personalizado da Empresa do GHL (business.id_gestionale), e então:

  1. Se a Empresa existe: Atualizar seus Campos Personalizados financeiros via PUT.

  2. Se a Empresa é nova: Criar um novo perfil de negócios via POST.

A Configuração do Fluxo Atual:

  1. Gatilho / Ler Arquivo: Importa o CSV com colunas como codice_cliente, ragione_sociale, totale_ecopots, totale_lechuza, etc.

  2. Requisição HTTP (GET): Chama a API V2 do GHL usando Autenticação de Header (Authorization: Bearer pit-... e Version: 2021-04-15).

  3. Nó If: Verifica se a empresa foi encontrada.

  4. Requisição HTTP (POST / PUT): Nós HTTP padrão para rotear os dados.

Os Obstáculos que estamos enfrentando:

Problema 1: Falha na Pesquisa GET (Limitação da API V2)

Não conseguimos filtrar a requisição GET diretamente pelo nosso Campo Personalizado.

  • Se usarmos https://services.leadconnectorhq.com/businesses/search?locationId=XXXX&query={{ $json.codice_cliente }} retorna um array vazio [] porque a consulta global do GHL não indexa campos personalizados numéricos.

  • Se tentarmos adicionar o filtro de campo personalizado na string de consulta (&customFields=[...]), o GHL retorna um Erro 422: "property customFields should not exist".

  • Se mudarmos para uma busca global (/businesses/?limit=100), o n8n puxa um enorme payload de 1.2MB contendo todas as empresas, o que sobrescreve o contexto binário/JSON das linhas CSV originais recebidas, tornando o mapeamento subsequente extremamente complexo.

Problema 2: O Loop de Autorização POST (Erro 401)

Quando um registro segue o caminho POST para criar uma nova empresa (https://services.leadconnectorhq.com/businesses/), o GHL retorna:

401 - {"message":"LocationId is not specified","error":"Unauthorized","statusCode":401}

Mesmo que locationId seja explicitamente passado dentro do corpo JSON, e o mesmo token Bearer pit-... funciona perfeitamente para requisições GET e PUT em IDs existentes. Parece que as chaves de API no nível de localização do GHL são completamente restritas ao usar POST no endpoint /businesses/ a menos que se use um App Marketplace OAuth completo.

Alternativa Proposta (Mudar para Contatos + Nome da Empresa):

Como a escrita no endpoint /businesses/ parece altamente restrita com chaves de API padrão, estamos avaliando uma mudança para o endpoint Contatos.

Estamos pensando em criar/atualizar Contatos, passando o ID do ERP e campos de fatura dentro dos Campos Personalizados do Contato, enquanto mapeamos o campo companyName dentro do objeto de contato para vinculá-lo à empresa.

Minhas Perguntas para a Comunidade:

  1. Qual é a melhor prática no n8n para consultar uma Empresa do GHL usando um valor de Campo Personalizado sem buscar o banco de dados inteiro de 1.2MB ou esbarrar em erros 422?

  2. Como podemos contornar o comportamento de sobrescrita de dados após um nó de Requisição HTTP para que os nós PUT/POST finais ainda tenham acesso aos dados da linha CSV original (totais de faturas)? Devemos usar o novo nó Comparar Datasets / Mesclar (Enriquecer Dados Existentes) logo após a ingestão do arquivo?

  3. Alguém já executou com sucesso um POST para /businesses/ usando uma Chave de API de Localização padrão, ou uma integração OAuth completa é obrigatória para criar empresas?

  4. Regarding our alternative: Você acha que trocar para o endpoint de Contatos (enquanto popula o campo companyName dentro do contato) é uma solução sólida e confiável para contornar as restrições da API do endpoint de Negócios, ou existem caminhos arquiteturais melhores que você sugeriria?

Qualquer conselho, solução alternativa ou trecho JSON de fluxo seria muito apreciado!

Dedi, isso parece ser dois bloqueadores separados, não um bug do GHL: encontrar a empresa certa por codice_cliente e depois criar/atualizar sem o corpo da POST ser rejeitado. Se esse ID do ERP só existe em um campo personalizado da empresa que a busca do GHL não consegue filtrar, o GHL pode não ser uma fonte de busca confiável; mantenha um pequeno mapa codice_cliente -> businessId nos seus próprios dados e atualize por businessId.

Antes de reconstruir em torno de Contacts, execute uma verificação segura: envie uma solicitação mínima de criar empresa com o mesmo token e locationId, mas sem o payload de fatura/campo personalizado. Ainda retorna LocationId is not specified, ou apenas quando o corpo completo mapeado por CSV é enviado?

Esse é um problema muito bem documentado e você já fez a maior parte do trabalho diagnóstico por conta própria. Deixe-me analisar cada questão.

Sobre a limitação de lookup GET — a forma mais limpa de contornar é buscar todos os negócios uma vez no início do fluxo usando paginação, depois usar um nó Code para filtrar em memória pelo valor do seu campo personalizado. Sim, é um payload grande, mas você o chama apenas uma vez por execução do fluxo, não uma vez por linha do CSV. Algo como:

javascript

const allBusinesses = $input.all();
const target = allBusinesses.find(b => 
  b.json.customFields?.find(f => f.key === 'id_gestionale' && f.value === $('CSV Node').item.json.codice_cliente)
);

return target;

Sobre a preservação de dados do CSV após o nó HTTP Request — use um nó Merge configurado no modo “Combine” após sua chamada GET. Alimente tanto o item CSV original quanto a resposta HTTP nele. Dessa forma, seus nós PUT/POST a jusante ainda terão acesso a todos os campos de fatura originais ao lado dos dados de resposta do GHL. Esse é o padrão comum para essa situação exata.

Sobre o erro POST 401: você está certo e essa é uma limitação confirmada do GHL. As chaves de API de Localização padrão (pit-...) não têm permissão para criar novos registros de Business via POST. Esse endpoint requer um token OAuth completo ou um token de Integração Privada com escopo elevado. Se OAuth completo não for uma opção agora, sua mudança para Contacts é na verdade o caminho mais prático a seguir.

Sobre a mudança para Contacts: é uma solução sólida e amplamente usada. Preencha companyName no contato e o GHL o associará automaticamente ao Business correspondente. Armazene seu codice_cliente e totais de fatura em campos personalizados de Contact e você ignora completamente as restrições do endpoint Business. A principal desvantagem é que seus dados financeiros ficam no nível do Contact em vez do nível do Business, o que pode afetar os relatórios dependendo de como sua conta GHL está configurada.

Se manter dados no nível Business é importante a longo prazo, o ajuste apropriado é configurar um App do GHL Marketplace com OAuth, mas esse é um investimento maior e a abordagem de Contacts o desbloqueará hoje.

Você tem dois erros separados aqui e é útil corrigi-los um de cada vez, porque um 401 e um 422 significam coisas muito diferentes.

O 401 é autenticação ou escopo. Os tokens da GHL API V2 são delimitados por recurso, e escrever campos personalizados de Company/Business precisa especificamente do escopo de escrita de businesses, que é fácil de perder se seu token foi configurado para contacts. Confirme que o token (ou o app OAuth atrás dele) realmente tem o escopo de escrita de businesses (companies), e que você está na URL base V2 com o header Version correto que a GHL exige — um header de versão de API ausente ou incorreto sozinho pode gerar um 401 na V2.

O 422 é payload. Os campos personalizados da GHL precisam ser referenciados pelo seu ID de campo, não pelo nome do campo, e o formato do body para campos personalizados é específico (um array de objetos com o ID do campo e o valor, não uma chave simples). Se você está enviando codice_cliente por nome ou como uma propriedade de nível superior, esse é seu 422. Obtenha primeiro as definições de custom-field da company para pegar o ID de campo exato, depois escreva usando esse ID. Também confirme que o tipo de valor corresponde ao tipo do campo — enviar uma string em um campo de número gera 422 também.

Então: corrija o escopo e o header de versão para o 401, depois combine o ID do custom-field exato e o formato do body para o 422. Qual deles é disparado primeiro quando você executa, o 401? Esse tem que ser resolvido antes que o 422 seja sequer alcançável.

[RESOLVIDO] Sincronização Incremental de Contatos de Vendas de CSV para GoHighLevel em Múltiplos/Contatos Duplicados com n8n

Olá a todos! Gostaria de compartilhar uma solução para um problema que enfrentamos ao atualizar incrementalmente dados de vendas específicos por marca em nível anual de um arquivo CSV para o GoHighLevel (GHL). Isso é especialmente útil quando seu CRM contém múltiplos ou contatos duplicados (por exemplo, diferentes funcionários ou gerentes de filiais) pertencentes à mesma conta da empresa.

Após vários testes, construímos com sucesso um fluxo de trabalho linear e resiliente que agrega dados de forma limpa no início e os descompacta a jusante para atualizar simultaneamente todos os contatos duplicados correspondentes no GoHighLevel.

:world_map: O Que o Fluxo de Trabalho Faz e Como Funciona

O fluxo de trabalho foi desenvolvido para automatizar importações de dados de sistemas contábeis ou de gestão de documentos. Ele calcula métricas de vendas progressivas ano após ano divididas por marcas de produtos individuais e atualiza dinamicamente a “Data da Última Compra”.

Aqui está a arquitetura lógica exata passo a passo:

1. Extração de Dados e Agregação a Montante (CSV)

  • Análise Resiliente: O fluxo de trabalho ingere um arquivo CSV (via Google Drive ou um Acionador Manual). As exportações contábeis geralmente envolvem campos de dados entre aspas duplas rígidas que quebram analisadores CSV nativos. O primeiro nó JavaScript limpa a formatação decimal (manipulando vírgulas) e usa um script de análise de linhas personalizado para reconstruir um objeto de dados limpo de 8 colunas.

  • Consolidação a Montante: Se o CSV contém múltiplas faturas ou notas de entrega para o mesmo cliente no mesmo arquivo, o código soma programaticamente os totais, gerando um único registro cumulativo por ID de loja exclusivo. Isso evita que o n8n dispare solicitações paralelas assincronicamente para o mesmo cliente ao GHL, evitando bloqueios de taxa de API ou condições de corrida (onde as operações sobrescrevem uma à outra).

2. Busca de Contato no CRM (GET)

  • O fluxo de trabalho envia uma solicitação GET para o endpoint /contacts/ do GoHighLevel. Para contornar variações ou discrepâncias em endereços de e-mail corporativos entre entradas duplicadas, a consulta busca usando o Nome da Empresa (ou um ID de Loja Externa exclusivo).

  • O GoHighLevel responde com um payload JSON contendo um array contacts listando cada contato que corresponde a essa consulta de empresa específica.

3. O Bifurcador Lógico (Mesclar e SE)

  • Um nó Mesclar emparelha os dados CSV agregados frescos com a saída da busca do GHL.

  • Um nó SE avalia se a empresa já existe no CRM (meta.total > 0). Se o cliente é novo, o fluxo de trabalho se ramifica para o caminho FALSO para criar um novo registro (POST). Se existe, ele segue o caminho VERDADEIRO para atualizações.

4. Descompactação de Duplicatas e Linearização (Descompactar Duplicatas)

  • Esse é o segredo para lidar com registros duplicados. Posicionado imediatamente após a saída VERDADEIRA do nó SE, um nó JavaScript dedicado isola o array de contatos retornado pelo GHL e o “desdobra” em linhas de execução independentes para o n8n.

  • Por exemplo, se 3 perfis de contato duplicados existem sob o mesmo nome de empresa no GoHighLevel, esse nó instantaneamente converte esse único thread de execução em 3 itens separados fluindo a jusante em paralelo.

5. Cálculo Incremental (Matemática de Marcas)

  • O nó JavaScript subsequente processa esses contatos descompactados um a um. Ele procura dentro dos campos personalizados daquele Contact ID específico (por exemplo, totale_ecopots_2026, totale_lechuza_2026), extrai o valor numérico histórico atual e matematicamente adiciona o novo volume de vendas calculado do arquivo CSV atual. Ele gera um payload limpo e pronto para salvar.

6. Atualizações em Massa de Campos Personalizados (PUT)

  • O nó HTTP Request final executa um comando PUT apontado para uma string de URL dinâmica: https://services.leadconnectorhq.com/contacts/``{{ $json.id_contatto }}.

  • Ao desativar a chave Executar Uma Vez nas configurações avançadas do nó, o n8n executa um loop sequencial perfeito. Ele dispara tantas solicitações PUT individuais quanto há perfis duplicados gerados pelo código de descompactação, atualizando perfeitamente as métricas históricas em cada perfil de representante vinculado àquela conta no CRM.

Obrigado a todos pelas dicas anteriores. Espero que esse esquema estrutural ajude qualquer pessoa lidando com cálculos incrementais complexos em perfis de banco de dados duplicados!