Resumen
Para saber cuántos clientes te trae el SEO hay que guardar cada contacto (formulario enviado, clic para llamar, clic a WhatsApp, reserva cerrada) en tu propia base de datos junto con la fuente de tráfico de la visita que lo produjo. La fuente se decide al entrar, con el referrer y los parámetros UTM o gclid, y se arrastra durante toda la sesión. Después se cuenta en un panel: contactos por fuente, mes a mes, con las búsquedas de Search Console al lado de «Google orgánico». Con eso ya no preguntas cuántas visitas trajo Google; ves cuántas personas te escribieron desde Google.
Por qué el informe de GA4 no te dice cuántos clientes trae el SEO
Casi todos los clientes que me llegan tienen GA4 instalado y casi ninguno sabe cuántos contactos le trae Google. GA4 da sesiones por canal y, con eventos, totales de clics. No da la lista: quién escribió, cuándo y desde qué página.
Y muchos «leads» de GA4 no son leads. En un cliente de Lima, tocar el botón de WhatsApp contaba como generate_lead. Cuando la conversión pasó a ser «dejó nombre y teléfono», los leads bajaron, y eran los de verdad. Por eso monto una medición propia en paralelo: GA4 queda para la historia y el conteo vive en una tabla mía.
Qué cuenta como contacto
Son cuatro vías, todas con la misma procedencia:
| Vía | Qué se escucha en la página | Qué se guarda |
|---|---|---|
| Formulario | El submit y los eventos AJAX de Contact Form 7, Elementor, WPForms y Gravity Forms | Nombre, teléfono, correo y mensaje, leídos de los campos |
| Llamada | Clic en cualquier a[href^="tel:"] | El número pulsado (sirve para saber qué sucursal) |
Clic en enlaces de api.whatsapp.com, wa.me o whatsapp: | Página y botón; el nombre si lo deja | |
| Reserva | El mensaje que el motor de reservas manda a la página al confirmar | Código, fecha, hora y número de personas |
Regla que aprendí a golpes: no se intercepta nada. El script anota con navigator.sendBeacon, que entrega la petición aunque la página se vaya a WhatsApp, y deja que el enlace haga lo suyo. El buscador, el login y el carrito quedan fuera.
Cómo se decide la fuente de cada contacto
La fuente se calcula en la primera página vista y se guarda con un identificador de sesión. Si esa persona llama veinte minutos después, el clic hereda la fuente con la que llegó. El orden de las reglas importa:
- Si la URL trae
gclid,gbraid,wbraido unutm_mediumtipocpc, es publicidad. Va primero porque un anuncio de Google también llega con referrer de google.com. - Si trae
utm_source, manda la campaña: la ficha de Google Maps (si el UTM dice gbp, maps o ficha), email, redes o lo que corresponda. - Si no hay UTM, se mira el dominio del referrer. Los asistentes de IA van aparte con su nombre: chatgpt.com, perplexity.ai, gemini.google.com, copilot.microsoft.com y nueve más. Luego Google (separando
/maps), Bing, DuckDuckGo, Ecosia; luego redes; luego guías del sector. - Sin referrer ni parámetros es directo. Con una excepción que nos costó encontrar: si la URL trae
srsltid, viene de Google aunque el referrer esté vacío.
Separar la IA vale la pena. En las dos webs de Lima donde lo comprobé, entre el 7 de septiembre y el 6 de octubre de 2026, las visitas desde ChatGPT terminaron en contacto bastante más que las de Google orgánico: 14,8 % contra 1,7 % en una y 11,8 % contra 3,3 % en la otra (GA4, sesiones con un evento clave). Si te interesa ese tráfico, en SEO para IA explico cómo lo trabajo.
El caso difícil: reservas dentro de un iframe
Un restaurante en Madrid con tres locales me pidió en octubre ver cada reserva con su fuente. El motor vive en un iframe de otro dominio, así que la página no puede leer lo que pasa dentro. Revisando su JavaScript encontré que, al confirmar, hace parent.postMessage({reserv:{token, date, time, pax}}, '*'). La página escucha message, comprueba que el origen sea el dominio del motor, averigua de qué local es la reserva por el src del iframe y la manda a la base con la fuente de la sesión. Para el paso anterior (alguien abrió el motor pero no reservó) uso el blur de la ventana: si el foco pasa a ese iframe, se anota «motor abierto».
Hay que contarle al cliente dos límites. Las reservas desde el botón de Google Maps, Instagram o la web del motor no pasan por su sitio. Y como es España, no hay cookies de medición: la sesión vive en sessionStorage, sin IP ni ciudad.

Cómo se ve el panel (y por qué nunca enseña ceros)
La medición propia del restaurante arrancó el 7 de octubre de 2026. Contando solo desde ahí, el primer día habría salido una fila de ceros. Me pasó en septiembre con otros dos paneles: los ceros eran correctos, había una nota, y aun así se leía «no funciona».
Desde entonces cada contador suma la historia de GA4 hasta el último día cerrado y la medición propia desde su arranque, con un corte limpio por fecha. Lo que no tiene registros va en una línea al pie, no en una tarjeta en cero. Septiembre, con la historia de GA4:

La búsqueda orgánica (casi toda de Google: 1.549 de esas visitas) dio 1.621 visitas y 199 clics en «Reservar» (12,3 por cada 100). Los anuncios convierten más por visita (16,4) pero traen menos (155). Y la tendencia: el orgánico pasó de 106 clics en agosto a 199 en septiembre, y el sitio entero de 307 a 511.

Ojo: un clic en «Reservar» no es una reserva cerrada. La confirmada por postMessage solo existe desde la medición propia, y el panel las separa.
Errores que nos costaron leads
La hora de WordPress contra la de MySQL. El 24 de septiembre un panel enseñaba 2 contactos de una web de abogados y había 11 en la base. Otro enseñaba 14 y eran 33. Las filas se escribían con current_time('mysql'), que es hora local de WordPress, y se filtraban con NOW(), que es la hora del servidor de base de datos en UTC. Con cinco horas de desfase, lo del día quedaba «en el futuro» y desaparecía sin error. Busqué el fallo en el JavaScript y estaba en el SQL. Ahora los límites se calculan en PHP con la misma hora con la que se escribe.
La puerta antes de WhatsApp. Un sitio exigía nombre y teléfono antes de abrir el chat. El panel decía 8 clics y 3 leads. Los otros 5 cerraron el modal y nunca escribieron. Ahora el clic se guarda primero, con su fuente, y el nombre se pide sin exigirlo.
Sumar peras con manzanas. En una tienda de iluminación, los formularios (orgánicos) y los WhatsApp de la landing de campaña van en tablas separadas. Juntos en un solo «contactos», no sabrías qué pagó el SEO y qué los anuncios.
Si tu panel nunca falla, desconfía: un fetch sin revisar r.ok dice «todo bien» mientras el contacto se pierde.
Juntarlo con Search Console
Al lado de «Google orgánico» pongo las páginas de entrada que produjeron contactos y sus búsquedas de Search Console. Así el cliente ve qué escribe en Google la gente que acaba llamando. Nada de informes de posiciones.
Search Console va dos o tres días por detrás, así que la comparación se recorta a los días que ya tiene, y las consultas de ventanas de 28 días no se suman entre fechas. Un proceso en el servidor lo actualiza cada madrugada.
En Basher, la agencia de iGaming donde trabajo, el panel suma grabaciones de sesión, mapas de calor y embudos junto a las keywords que el cliente quiere ganar. Un negocio local no necesita tanto: con contactos por fuente y las búsquedas al lado ya sabes si el SEO paga.
Lo monto dentro del servicio de SEO. Si dudas dónde poner el presupuesto, en SEO o Google Ads comparo los dos.
Preguntas frecuentes
¿Puedo medir los leads del SEO solo con Google Analytics 4?
Ves totales de eventos por canal, no la lista de contactos con su fuente. Y muchos eventos marcados como lead son simples clics. Lo fiable es una tabla propia, con GA4 para la historia.
¿Cómo sé si una llamada vino de Google orgánico?
Se mide el clic en el enlace tel: y se le asigna la fuente que tenía la sesión al entrar (referrer de Google sin gclid ni UTM de pago). Las llamadas de quien marca el número a mano desde la ficha de Maps no pasan por la web y hay que contarlas aparte.
¿Se puede saber qué reservas vienen del SEO si el motor de reservas es un iframe?
Sí, si el motor avisa a la página al confirmar. En el caso que cuento, el motor manda un postMessage con código, fecha, hora y personas; la página lo escucha, valida el origen y lo guarda con la fuente de la sesión.
¿Por qué mi panel de leads muestra menos contactos de los que hay?
Lo más común es la zona horaria: se escribe en hora local, se filtra con la UTC del servidor y lo reciente queda fuera. Compara la lista sin filtro de fechas con la de 7 días; si la abierta trae más, es eso.