GoHighLevel 如何判斷每位潛在客戶的實際來源渠道,以及 utmSessionSource 為何會誤導
Cómo saber el canal real de cada lead en GoHighLevel (y por qué utmSessionSource te engaña)
GoHighLevel 的 utmSessionSource 不能可靠區分社交媒體付費廣告與自然流量;文章示範使用聯絡人 attributions 資料及 Python API 腳本分類來源渠道。
Si trabajas con GoHighLevel (GHL) y alguna vez has intentado responder a la pregunta «¿de dónde vienen mis leads?», probablemente hayas mirado el campo Session Source del contacto. Y probablemente te haya dado una respuesta equivocada.
En nuestro caso, el CRM etiquetaba como Social media a casi todos los contactos que entraban por anuncios de Meta. Es decir, mezclaba en el mismo saco a quien llega desde un anuncio de pago y a quien llega desde un post orgánico de Instagram. Para decidir dónde invertir, esa diferencia lo es todo.
En este artículo explico qué datos de atribución guarda GHL de verdad, por qué utmSessionSource no sirve para separar pago de orgánico y cómo clasificar cada contacto con un script corto en Python usando la API.
Qué guarda GHL en cada contacto
Cuando un contacto entra por un formulario, un calendario o un DM, GHL guarda un array attributions en el contacto. Cada elemento es un «toque» y trae estos campos (los he sacado de la respuesta real de la API v2):
isFirst, isLast -> primer y último toque
medium, mediumId -> form, calendar, instagram (DM)...
pageUrl, url -> página donde se convirtió
referrer -> de dónde venía el navegador
utmSource, utmMedium, utmCampaign, utmContent, utmTerm, utmKeyword
utmFbclid -> identificador de clic de Meta
utmSessionSource -> la etiqueta «bonita» que ves en el CRM
ip, userAgent
El problema está en el último campo de la lista que mira todo el mundo: utmSessionSource. Es una etiqueta calculada por GHL con valores como Social media, Referral, Direct traffic o Paid search, y no distingue pago de orgánico en redes sociales. Un clic en un anuncio de Facebook con fbclid y utm_medium=paid puede acabar etiquetado como Social media, igual que alguien que pinchó el enlace de tu bio.
Las señales que sí sirven
Para saber el canal real hay que mirar los datos crudos del toque, por este orden:
| Señal | Qué indica |
|---|---|
utmFbclid presente, o fbclid en la pageUrl
|
Clic en un anuncio de Meta (pago) |
gclid / gbraid / wbraid en la pageUrl
|
Clic en Google Ads (pago) |
utmMedium = paid, cpc, ppc… |
Campaña de pago etiquetada |
medium = instagram
|
Conversación por DM de Instagram |
referrer de Google, Bing, DuckDuckGo… sin parámetros de clic |
Búsqueda orgánica |
referrer = android-app://com.instagram.android, l.facebook.com… sin fbclid
|
Red social orgánica |
referrer = android-app://com.openai.chatgpt o chatgpt.com
|
Tráfico desde ChatGPT |
| Sin referrer ni parámetros | Directo (o referrer perdido) |
Dos detalles que nos costó ver:
-
El
fbclidmanda sobre el referrer. Un clic en un anuncio que se abre en el navegador interno de Instagram llega conreferrerde Instagram. Si solo miras el referrer, lo cuentas como orgánico. -
utmSourcepuede venir abreviado. En nuestras campañas de Meta aparecen valores comofb,igoan(Audience Network). Si filtras porfacebooka secas, se te escapan.
El script
Este script recorre todos los contactos de una subcuenta, se queda con el primer toque de cada uno y lo clasifica con las reglas de arriba. Solo necesita un token privado con permiso de lectura de contactos (contacts.readonly).
import os
import requests
from collections import Counter
from urllib.parse import urlparse, parse_qs
TOKEN = os.environ["GHL_TOKEN"]
LOCATION_ID = os.environ["GHL_LOCATION_ID"]
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Version": "2021-07-28"}
SEARCH = ("google.", "bing.", "duckduckgo.", "yahoo.", "ecosia.")
SOCIAL = ("facebook.", "instagram.", "com.instagram.android", "linkedin.", "tiktok.", "t.co")
AI = ("chatgpt.com", "com.openai.chatgpt", "perplexity.ai", "gemini.google")
def contactos():
url = f"https://services.leadconnectorhq.com/contacts/?locationId={LOCATION_ID}&limit=100"
while url:
r = requests.get(url, headers=HEADERS, timeout=30)
r.raise_for_status()
data = r.json()
yield from data.get("contacts", [])
url = data.get("meta", {}).get("nextPageUrl")
def params(url):
if not url:
return {}
return {k.lower(): v for k, v in parse_qs(urlparse(url).query).items()}
def canal(toque):
q = params(toque.get("pageUrl") or toque.get("url"))
ref = (toque.get("referrer") or "").lower()
medium = (toque.get("utmMedium") or "").lower()
if toque.get("utmFbclid") or "fbclid" in q:
return "Meta Ads"
if q.keys() & {"gclid", "gbraid", "wbraid"}:
return "Google Ads"
if medium in {"paid", "cpc", "ppc", "paid_social"}:
return f"Pago ({toque.get('utmSource') or 'sin fuente'})"
if (toque.get("medium") or "").lower() == "instagram":
return "DM de Instagram"
if any(s in ref for s in AI):
return "IA (ChatGPT, Perplexity...)"
if any(s in ref for s in SEARCH):
return "Orgánico (buscadores)"
if any(s in ref for s in SOCIAL):
return "Redes sociales orgánico"
if ref:
return "Referencia"
return "Directo / sin datos"
def primer_toque(contacto):
toques = contacto.get("attributions") or []
for t in toques:
if t.get("isFirst"):
return t
return toques[0] if toques else None
resumen = Counter()
for c in contactos():
t = primer_toque(c)
resumen[canal(t) if t else "Sin atribución"] += 1
for nombre, total in resumen.most_common():
print(f"{nombre:32} {total}")
La salida es un recuento por canal real. Si quieres el último toque en lugar del primero, cambia isFirst por isLast: el primero te dice qué canal descubre al cliente y el último, cuál lo convierte. Merece la pena mirar los dos.
Por qué no usar las Smart Lists
Lo primero que probamos fue filtrar en las Smart Lists de GHL por los campos de atribución. Los filtros existen, pero en la práctica no devolvían resultados fiables para estos casos (por ejemplo, separar por fbclid). Ir a la API es más trabajo la primera vez, pero el resultado es reproducible y se puede programar para que se ejecute cada semana.
Limitaciones que conviene tener en cuenta
- El referrer no siempre llega. Algunos navegadores y apps lo recortan o lo eliminan. Por eso el bloque «Directo / sin datos» nunca es cero.
- Solo ves lo que pasa por formularios, calendarios o chats de GHL. Si el lead entra por una llamada o un WhatsApp sin enlace rastreado, no habrá toque.
-
Etiqueta tus campañas igual siempre. Si un mes usas
utm_medium=paidy otrocpc, el script lo resuelve, pero el informe de campañas de cualquier otra herramienta no.
En resumen
utmSessionSource es cómodo para un vistazo, pero no sirve para decidir presupuesto. Los datos buenos están en attributions: utmFbclid, los parámetros de la pageUrl y el referrer. Con 80 líneas de Python tienes una foto honesta de qué canal trae cada contacto.
Si trabajas con GHL y te interesa cómo lo montamos en proyectos de captación (CRM, automatizaciones y medición), lo contamos en nuestra página sobre GoHighLevel.
來源:DEV Community · dev.to