Hola a todos,
Estoy construyendo un flujo de trabajo de sincronización en n8n (versión Cloud 2.22.6) para actualizar datos de ventas de nuestro ERP a perfiles comerciales de GoHighLevel (GHL) (Empresas).
Mi objetivo:
Procesamos un archivo CSV que contiene totales de facturas divididos por marca. Necesitamos buscar la empresa en GHL usando un ID externo inmutable del ERP (codice_cliente) almacenado dentro de un campo personalizado de GHL Company (business.id_gestionale), y luego:
-
Si la empresa existe: Actualizar sus campos personalizados financieros mediante PUT.
-
Si la empresa es nueva: Crear un nuevo perfil de empresa mediante POST.
La configuración actual del flujo de trabajo:
-
Activador / Leer archivo: Importa el CSV con columnas como codice_cliente, ragione_sociale, totale_ecopots, totale_lechuza, etc.
-
Solicitud HTTP (GET): Llama a la API V2 de GHL usando autenticación por encabezado (Authorization: Bearer pit-... y Version: 2021-04-15).
-
Nodo Si: Verifica si se encontró la empresa.
-
Solicitud HTTP (POST / PUT): Nodos HTTP estándar para enrutar los datos.
Los obstáculos que enfrentamos:
Problema 1: Error de búsqueda GET (Limitación de API V2)
No podemos filtrar la solicitud GET directamente por nuestro campo personalizado.
-
Si usamos https://services.leadconnectorhq.com/businesses/search?locationId=XXXX&query={{ $json.codice_cliente }} devuelve un arreglo vacío [] porque la consulta global de GHL no indexa campos personalizados numéricos.
-
Si intentamos agregar el filtro de campo personalizado en la cadena de consulta (&customFields=[...]), GHL devuelve un error 422: "property customFields should not exist".
-
Si pasamos a una búsqueda global (/businesses/?limit=100), n8n extrae una carga masiva de 1.2 MB que contiene todas las empresas, lo que sobrescribe el contexto binario/JSON de las filas CSV originales entrantes, haciendo que el mapeo posterior sea extremadamente complejo.
Problema 2: El bucle de autorización POST (Error 401)
Cuando un registro baja por la ruta POST para crear una nueva empresa (https://services.leadconnectorhq.com/businesses/), GHL devuelve:
401 - {"message":"LocationId is not specified","error":"Unauthorized","statusCode":401}
Aunque locationId se pase explícitamente dentro del cuerpo JSON, y el mismo token Bearer pit-... funciona perfectamente para solicitudes GET y PUT en IDs existentes. Parece que las claves API de nivel de ubicación de GHL están completamente restringidas para usar POST en el extremo /businesses/ a menos que se use una aplicación completa de OAuth Marketplace.
Alternativa propuesta (Cambio a Contactos + Nombre de empresa):
Ya que escribir en el extremo /businesses/ parece estar muy restringido con claves API estándar, estamos evaluando un cambio al extremo Contactos.
Estamos pensando en crear/actualizar Contactos en su lugar, pasando el ID del ERP y los campos de factura dentro de los campos personalizados del contacto, mientras asignamos el campo companyName dentro del objeto de contacto para vincularlo a la empresa.
Mis preguntas a la comunidad:
-
¿Cuál es la mejor práctica en n8n para consultar una empresa GHL usando un valor de campo personalizado sin obtener toda la base de datos de 1.2 MB o encontrarse con errores 422?
-
¿Cómo podemos evitar el comportamiento de sobrescritura de datos después de un nodo de solicitud HTTP para que los nodos PUT/POST finales aún tengan acceso a los datos originales de la fila CSV (totales de facturas)? ¿Deberíamos usar el nuevo nodo Comparar conjuntos de datos / Combinar (enriquecer datos existentes) justo después de la ingesta de archivos?
-
¿Alguien ha ejecutado exitosamente un POST a /businesses/ usando una clave API de ubicación estándar, o es obligatoria una integración OAuth completa para crear empresas?
-
Con respecto a nuestra alternativa: ¿Creen que cambiar al extremo de contactos (mientras se completa el campo companyName dentro del contacto) es una solución sólida y confiable para evitar las restricciones de la API del extremo de negocios, o hay caminos arquitectónicos mejores que sugieran?
¡Cualquier consejo, solución alternativa o fragmento de JSON del flujo de trabajo sería muy apreciado!
Dedi, esto parece ser dos bloqueos separados, no un único bug de GHL: encontrar la empresa correcta por codice_cliente, y luego crearla/actualizarla sin que se rechace el cuerpo de la solicitud POST. Si ese ID de ERP solo existe en un campo personalizado de empresa que la búsqueda de GHL no puede filtrar, GHL podría no ser una fuente de búsqueda confiable; mantén un pequeño mapa codice_cliente -> businessId en tus propios datos y actualiza por businessId.
Antes de reconstruir alrededor de Contacts, ejecuta una verificación segura: envía una solicitud de creación de empresa mínima con el mismo token y locationId, pero sin la carga de facturación/campos personalizados. ¿Sigue devolviendo LocationId is not specified, o solo cuando se envía el cuerpo completo mapeado desde CSV?
Este es un problema realmente bien documentado y ya has hecho la mayor parte del trabajo de diagnóstico. Permíteme pasar por cada problema.
Sobre la limitación de búsqueda GET: la solución más limpia es obtener todos los negocios una vez al inicio del flujo usando paginación, luego usar un nodo Code para filtrar en memoria por el valor de tu campo personalizado. Sí, es una carga útil grande, pero solo la llamas una vez por ejecución del flujo, no una vez por fila 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 preservar datos CSV después del nodo HTTP Request: usa un nodo Merge establecido en modo “Combine” después de tu llamada GET. Alimenta tanto el elemento CSV original como la respuesta HTTP en él. De esta manera, tus nodos PUT/POST posteriores aún tienen acceso a todos los campos de factura originales junto con los datos de respuesta de GHL. Este es el patrón estándar para esta situación exacta.
Sobre el error POST 401: tienes razón y esta es una limitación confirmada de GHL. Las claves API de ubicación estándar (pit-...) no tienen permiso para crear nuevos registros de Negocio mediante POST. Ese extremo requiere un token de aplicación OAuth completo o un token de integración privada con alcance elevado. Si OAuth completo no es una opción en este momento, tu pivote a Contactos es en realidad el camino más práctico a seguir.
Sobre el pivote de Contactos: es una solución sólida y ampliamente utilizada. Completa companyName en el contacto y GHL lo asociará automáticamente al Negocio coincidente. Almacena tu codice_cliente y los totales de facturas en campos personalizados de Contacto y así evitas completamente las restricciones del extremo Negocio. La principal compensación es que tus datos financieros viven a nivel de Contacto en lugar de a nivel de Negocio, lo que puede afectar los reportes dependiendo de cómo esté configurada tu cuenta de GHL.
Si mantener datos a nivel de Negocio es importante a largo plazo, la solución adecuada es configurar una aplicación de GHL Marketplace con OAuth, pero eso es una inversión más grande y el enfoque de Contactos te desbloqueará hoy.
Tienes dos errores separados aquí y es útil corregirlos uno a la vez, porque un 401 y un 422 significan cosas muy diferentes.
El 401 es autenticación o scope. Los tokens de la API V2 de GHL tienen un scope por recurso, y escribir campos personalizados de Company/Business necesita específicamente el scope de escritura de businesses, que es fácil de pasar por alto si tu token fue configurado para contactos. Confirma que el token (o la aplicación OAuth detrás de él) realmente tiene el scope de escritura de businesses (empresas), y que estás en la URL base V2 con el header de versión correcto que GHL requiere; un header de versión de API faltante o incorrecto solo puede lanzar un 401 en V2.
El 422 es payload. Los campos personalizados de GHL deben ser referenciados por su field id, no por el nombre del campo, y la forma del body para campos personalizados es específica (un array de objetos con el field id y el valor, no una clave plana). Si estás enviando codice_cliente por nombre o como una propiedad de nivel superior, ese es tu 422. Obtén primero las definiciones de custom-field de la empresa para conseguir el field id exacto, luego escribe usando ese id. También confirma que el tipo de valor coincida con el tipo del campo; enviar un string a un campo numérico también genera 422.
Entonces: corrige el scope y el header de versión para el 401, luego coincide con el field id exacto de custom-field y la forma del body para el 422. ¿Cuál se activa primero cuando lo ejecutas, el 401? Ese tiene que resolverse antes de que el 422 sea siquiera alcanzable.
[SOLUCIONADO] Sincronización Incremental de Contactos de Ventas desde CSV a GoHighLevel en Múltiples/Contactos Duplicados con n8n
¡Hola a todos! Quería compartir una solución a un problema que enfrentamos con respecto a la actualización incremental de datos de ventas anuales específicos de marca desde un archivo CSV a GoHighLevel (GHL). Esto es especialmente útil cuando tu CRM contiene múltiples o contactos duplicados (por ejemplo, diferentes empleados o gerentes de sucursal) pertenecientes a la misma cuenta de empresa.
Después de varias pruebas, construimos con éxito un flujo de trabajo lineal y resiliente que agrega datos de manera limpia por adelantado y los desempaqueta aguas abajo para actualizar cada contacto duplicado coincidente en GoHighLevel simultáneamente.
Qué Hace el Flujo de Trabajo y Cómo Funciona
El flujo de trabajo está diseñado para automatizar importaciones de datos desde sistemas de contabilidad o gestión de documentos. Calcula métricas de ventas progresivas año a año desglosadas por marcas de productos individuales y actualiza dinámicamente la «Fecha de Última Compra».
Aquí está la arquitectura lógica exacta paso a paso:
1. Extracción de Datos y Agregación Aguas Arriba (CSV)
-
Análisis Resiliente: El flujo de trabajo ingiere un archivo CSV (a través de Google Drive o un Disparador Manual). Las exportaciones contables a menudo envuelven campos de datos dentro de comillas dobles rígidas que rompen analizadores CSV nativos. El primer nodo JavaScript limpia el formato decimal (manejando comas) y usa un script de análisis de fila personalizado para reconstruir un objeto de datos limpio de 8 columnas.
-
Consolidación Aguas Arriba: Si el CSV contiene múltiples facturas o notas de entrega para el mismo cliente en el mismo archivo, el código suma programáticamente los totales, generando un registro acumulativo único por ID de tienda único. Esto evita que n8n dispare solicitudes paralelas asincrónicas para el mismo cliente a GHL, evitando bloqueos de velocidad de API o condiciones de carrera (donde las operaciones se sobrescriben mutuamente).
2. Búsqueda de Contacto en CRM (GET)
-
El flujo de trabajo envía una solicitud GET al endpoint /contacts/ de GoHighLevel. Para eludir variaciones o discrepancias en direcciones de correo electrónico corporativas en entradas duplicadas, la consulta busca usando el Nombre de la Empresa (o un ID de Tienda Externa único).
-
GoHighLevel responde con una carga útil JSON que contiene un array contacts que enumera cada único contacto que coincide con esa consulta de empresa específica.
3. La Bifurcación Lógica (Fusionar e IF)
-
Un nodo Merge empareja los datos CSV agregados frescos con la salida de búsqueda de GHL.
-
Un nodo IF evalúa si la empresa ya existe en el CRM (meta.total > 0). Si el cliente es nuevo, el flujo de trabajo se ramifica a la ruta FALSE para crear un nuevo registro (POST). Si existe, avanza a la ruta TRUE para actualizaciones.
4. Desempaquetamiento de Duplicados y Linealización (Scompatta Duplicati)
-
Este es el ingrediente secreto para manejar registros duplicados. Posicionado inmediatamente después de la salida TRUE del nodo IF, un nodo JavaScript dedicado aísla el array de contacto devuelto por GHL y lo “desenvuelve” en líneas de ejecución independientes para n8n.
-
Por ejemplo, si existen 3 perfiles de contacto duplicados bajo el mismo nombre de empresa en GoHighLevel, este nodo instantáneamente convierte ese único hilo de ejecución en 3 elementos separados fluyendo aguas abajo en paralelo.
5. Cálculo Incremental (Matemáticas de Marca)
- El nodo JavaScript subsiguiente procesa estos contactos desempaquetados uno por uno. Busca dentro de los campos personalizados de ese ID de Contacto específico (por ejemplo,
totale_ecopots_2026, totale_lechuza_2026), extrae el valor numérico histórico actual, y suma matemáticamente el volumen de ventas nuevo calculado desde el archivo CSV actual. Genera una carga útil limpia y lista para guardar.
6. Actualizaciones Masivas de Campos Personalizados (PUT)
-
El nodo HTTP Request final ejecuta un comando PUT apuntado a una cadena de URL dinámica: https://services.leadconnectorhq.com/contacts/``{{ $json.id_contatto }}.
-
Al desactivar el toggle Execute Once dentro de la configuración avanzada del nodo, n8n ejecuta un bucle secuencial perfecto. Dispara tantas solicitudes PUT individuales como hay perfiles duplicados generados por el código de desempaquetamiento, actualizando sin problemas métricas históricas en cada perfil de representante único vinculado a esa cuenta en el CRM.
¡Gracias a todos por los consejos anteriores. Espero que este esquema estructural ayude a cualquiera que trate con cálculos incrementales complejos sobre perfiles de base de datos duplicados!