13 min de lecturaIntegraciones

Webhooks vs polling para tracking de clics - elige el patrón correcto

Un desglose práctico de cuándo usar webhooks y cuándo hacer polling a la API de analítica para datos de clics: costes ocultos de cada enfoque, ejemplos concretos de código en TypeScript y Python, y el patrón híbrido que cubre la mayoría de los casos de uso en producción.

Sasha Ehrlich
Compliance · EU residency
Diagrama comparativo entre webhooks y polling para el seguimiento de clics: el panel izquierdo muestra al servidor enviando eventos a un endpoint receptor, con eventos click.created por clic planificados; el panel derecho muestra a cron consultando /analytics/summary cada N minutos

Dos equipos construyendo sobre la misma API de acortador de URL a menudo terminan con arquitecturas de integración completamente diferentes. Un equipo quiere un endpoint de webhook que reaccione a cada clic en tiempo real. El otro escribe un cron job que hace polling a la API de analítica cada cinco minutos. Ambos son diseños válidos, pero solo uno funciona hoy para los clics en Elido: los webhooks por clic están planificados, no publicados, así que la vía activa para los datos de clics es el polling. La decisión entre ellos tiene consecuencias reales para latencia, sobrecarga operacional, y cuánto se degrada tu sistema cuando algo sale mal.

Este post expone los trade-offs reales.

Los dos patrones

Polling

Polling significa que tu código pregunta a la API por datos recientes de clics en un horario. Un cron job se despierta, llama a /v1/workspaces/{workspace_id}/analytics/clicks/recent o /v1/workspaces/{workspace_id}/analytics/summary, procesa los resultados, y luego duerme hasta el siguiente intervalo.

El flujo de datos es basado en pull: tu infraestructura inicia cada interacción. El servidor de la API no tiene conocimiento de tus sistemas internos - solo responde a las consultas que envías.

Webhooks

Webhooks significa que el servidor de Elido envía un evento a tu endpoint HTTPS. Tu receptor lo maneja, devuelve un 2xx, y la entrega se registra como exitosa. Hoy esos eventos cubren enlaces y dominios (link.created, link.updated, link.expired, domain.verified y algunos más). Hay un evento click.created por clic planificado que aún no se emite, así que por ahora ningún webhook se activa cuando alguien hace clic.

El flujo de datos es basado en push: la plataforma inicia el contacto. Tu endpoint necesita ser alcanzable desde internet, necesita TLS, y necesita responder de forma fiable.

Diagrama lado a lado: el polling muestra tu cron job enviando GET a la API de Elido y extrayendo filas cada cinco minutos; los webhooks muestran el servidor de Elido enviando un POST a tu endpoint HTTPS que devuelve 2xx, con el evento click.created por clic aún planificado.

Cuándo el polling es la elección correcta

El polling se adapta a un conjunto específico de condiciones. Si la mayoría de estas aplican a tu situación, empieza con polling y recurre a webhooks solo cuando un problema concreto fuerce tu mano.

Controlas ambos lados de la integración. Cuando el consumidor es un dashboard o herramienta de reporting que tú posees y operas, el polling te da un comportamiento predecible y acotado. Tú decides el intervalo; tú decides la ventana de tiempo; tú decides cómo manejar resultados parciales.

Tu caso de uso es retrospectivo. Los informes semanales de campaña, los trabajos de agregación mensuales, y las pipelines de reconciliación no se benefician de latencia sub-minuto. Un cron job corriendo cada hora contra /summary o /breakdown/country es arquitectónicamente más simple y más fácil de razonar que un receptor de webhook con estado con manejo de reintentos.

No tienes un endpoint público que exponer. Los webhooks requieren una URL alcanzable desde la infraestructura de Elido. Si tu integración corre dentro de una red privada, una función Lambda sin URL estable, o la máquina local de un desarrollador, configurar un endpoint HTTPS entrante puede costar más en complejidad operacional que lo que vale el beneficio de latencia.

El volumen es bajo. A unos pocos miles de clics por día, la diferencia entre tiempo real y un retraso de cinco minutos rara vez es visible para los usuarios finales. El polling es simple de entender, simple de debuggear, y no produce sorpresas de infraestructura.

Cuándo los webhooks son la elección correcta

Los webhooks tienen sentido cuando la latencia es un requisito de producto en lugar de un nice-to-have. Para los datos de clics, lee esta sección como el plan para cuando click.created esté disponible; hasta entonces, los mismos casos de uso funcionan con un polling de intervalo corto a clicks/recent.

Estás construyendo un contador en vivo o UX en tiempo real. Si tu producto muestra a los usuarios un conteo de clics que se actualiza visiblemente en segundos después de una redirección, el polling en cualquier intervalo razonable se sentirá notablemente obsoleto. Un handler de webhook que incremente un contador de Redis en eventos click.created y lo muestre a través de una conexión WebSocket o SSE al frontend será la arquitectura que logrará esto sin martillar la API de analítica, una vez que ese evento esté disponible. Hoy, un polling de un minuto a clicks/recent que se detenga en la última fila que ya haya visto es lo más cercano que puedes conseguir.

Estás enriqueciendo registros de CRM por clic. Atar un evento de clic a un registro de contacto - identificar qué prospecto específico siguió el enlace en tu email de outbound y actualizar su timeline de CRM - es sensible al tiempo. Para cuando un trabajo de polling se ponga al día cinco minutos después, el representante de ventas puede haber ya llamado. Un handler de webhook que dispare una actualización de CRM en segundos del clic será la herramienta correcta cuando los eventos por clic estén disponibles; hasta entonces, mantén corto el intervalo de polling.

Estás ejecutando workflows event-driven. Los workflows disparados por eventos de clic - enviar un email de seguimiento cuando se clica un enlace, actualizar el segmento de un suscriptor, decrementar un conteo de inventario - son consumidores naturales de webhook. El evento click.created planificado está pensado para llevar suficientes datos para actuar inmediatamente, sin una consulta de ida y vuelta. Los workflows disparados por cambios en enlaces, como link.created o link.expired, pueden usar webhooks hoy.

Tienes un endpoint HTTPS estable y públicamente alcanzable. Este es el prerrequisito del que todo lo demás depende. Si ya tienes infraestructura de producción que acepta webhooks entrantes de otros proveedores (Stripe, GitHub, Twilio), añadir Elido al mismo receptor es de baja fricción.

Los costes ocultos de los webhooks

Los webhooks suenan simples: el servidor envía POST, tú lo manejas. La superficie de implementación real es mayor.

Diagrama en capas de las cuatro compuertas que pasa cada entrega de webhook entrante: verificación de firma HMAC, la ventana de replay de 300 segundos, deduplicación de idempotencia por el ID de entrega, y mantenerse disponible antes de devolver 2xx.

Verificación de firma

Elido firma cada entrega de webhook con HMAC-SHA256. El formato de firma es v1=HMAC-SHA256(secret, "${unix_timestamp}.${body}"), entregado en el header X-Elido-Signature y en X-Webhook-Signature como alias. La marca de tiempo se envía por separado en X-Webhook-Timestamp.

Debes verificar esta firma antes de procesar el payload; la guía para verificar las firmas de webhook también cubre Python, Go y la rotación de secretos. Un receptor que omite la verificación procesará cualquier POST que alcance el endpoint, incluyendo solicitudes spoofeadas de cualquiera que descubra tu URL de webhook.

Aquí hay un handler mínimo de Express en TypeScript que verifica la firma antes de hacer nada con el payload:

import express, { Request, Response } from "express";
import crypto from "crypto";

const app = express();

// Use raw body middleware - JSON parsers consume the stream before you can hash it
app.use("/webhook", express.raw({ type: "application/json" }));

function verifySignature(
  secret: string,
  signature: string,
  timestamp: string,
  rawBody: Buffer,
): boolean {
  const message = `${timestamp}.${rawBody.toString("utf8")}`;
  const expected =
    "v1=" + crypto.createHmac("sha256", secret).update(message).digest("hex");
  // Use timingSafeEqual to prevent timing-based enumeration
  return crypto.timingSafeEqual(
    Buffer.from(signature, "utf8"),
    Buffer.from(expected, "utf8"),
  );
}

app.post("/webhook", (req: Request, res: Response) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const timestamp = req.headers["x-webhook-timestamp"] as string;

  if (!signature || !timestamp) {
    return res.status(400).json({ error: "missing signature headers" });
  }

  // Reject payloads older than 5 minutes
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (age > 300) {
    return res.status(400).json({ error: "payload too old" });
  }

  if (
    !verifySignature(
      process.env.WEBHOOK_SECRET!,
      signature,
      timestamp,
      req.body as Buffer,
    )
  ) {
    return res.status(401).json({ error: "invalid signature" });
  }

  const event = JSON.parse((req.body as Buffer).toString("utf8"));

  if (event.type === "link.updated") {
    // Handle the link event
    console.log("link updated:", event.data);
  }

  // Always return 2xx promptly - do heavy processing async
  return res.status(200).json({ received: true });
});

La ventana de replay

La verificación de marca de tiempo en el ejemplo anterior aplica lo que la documentación de Elido llama una ventana de replay. Sin ella, un atacante que captura un solo payload firmado válido puede replayearlo indefinidamente - la firma sigue siendo válida para siempre porque se calcula a partir de una marca de tiempo fija. Con la verificación, un payload más viejo que cinco minutos es rechazado independientemente de si la firma es válida.

Configura la tolerancia a algo que tu infraestructura pueda manejar. Cinco minutos es el valor predeterminado convencional y coincide con lo que usa Stripe. Si tu receptor ocasionalmente se cae durante unos pocos minutos durante despliegues, esta ventana le da tiempo a volver y aún procesar entregas en vuelo.

Reintentos e idempotencia

Elido reintenta las entregas fallidas según un horario de backoff: el primer reintento llega 5 minutos después de un fallo y el segundo, 15 minutos después. Una entrega recibe 3 intentos en total de forma predeterminada. Después de 3 intentos fallidos la entrega se marca como permanentemente fallida y aparece en el registro de entregas del endpoint.

Esto significa que tu receptor puede recibir el mismo evento más de una vez. Cualquier procesamiento que tenga efectos secundarios - escribir a una base de datos, enviar un email, actualizar un contador - necesita ser idempotente. El header X-Webhook-Delivery lleva un ID de entrega estable que puedes usar como clave de idempotencia.

// Before processing, check whether this delivery has already been handled
const deliveryId = req.headers["x-webhook-delivery"] as string;
const alreadyProcessed = await redis.get(`webhook:delivery:${deliveryId}`);
if (alreadyProcessed) {
  return res.status(200).json({ received: true, duplicate: true });
}
// Mark as processed with a TTL that covers the retry window
await redis.set(`webhook:delivery:${deliveryId}`, "1", "EX", 3600);

Tu endpoint debe ser altamente disponible

La ventana de reintento es finita. Si tu receptor está caído más de aproximadamente 20 minutos (5 + 15), las entregas agotarán sus intentos y fallarán permanentemente. Para eventos donde la entrega garantizada importa - enriquecimiento de CRM, hooks de facturación - tu infraestructura de receptor necesita disponibilidad adecuada, no un servidor de hobbyista que ocasionalmente reinicia.

Este es el coste más subestimado de los webhooks para equipos nuevos en HTTP entrante. El polling se degrada con gracia: si el trabajo de polling falla, simplemente corre de nuevo en el siguiente intervalo y se pone al día. Un receptor de webhook que no está disponible pierde eventos permanentemente a menos que tengas una estrategia de reconciliación.

Los costes ocultos del polling

El polling parece simple desde fuera. Los costes reales se acumulan en producción.

El retraso es la restricción definitoria. Un cron job corriendo cada cinco minutos significa que los datos de clics están hasta cinco minutos obsoletos. Para la mayoría de los casos de uso retrospectivos esto es aceptable; para cualquier cosa user-facing, no lo es. Acortar el intervalo ayuda pero no elimina el retraso, y los intervalos muy cortos (menos de un minuto) empiezan a parecer martilleo de API en lugar de polling.

Solicitudes desperdiciadas. La mayoría de los intervalos de polling devuelven los mismos datos que la solicitud anterior. Si estás haciendo polling a un enlace de poco tráfico cada minuto y los clics llegan a aproximadamente uno por hora, 59 de cada 60 solicitudes no devuelven nada nuevo. Estas solicitudes aún cuentan contra tu límite de tasa de API.

Límites de tasa. La API de Elido aplica límites de tasa por workspace dimensionados por tier de facturación. Un trabajo de polling que corre frecuentemente a través de muchos enlaces en un workspace grande puede alcanzar estos límites, particularmente si otra automatización en el mismo workspace también está haciendo llamadas a la API. La API devuelve 429 Too Many Requests con un header X-RateLimit-Scope: workspace cuando esto sucede.

Paginación y eventos perdidos. El endpoint /clicks/recent usa paginación basada en cursor. Si haces polling en una ventana de tiempo fija - ?from=<last_poll>&to=<now> - y el volumen en esa ventana excede el tamaño de página, perderás eventos a menos que sigas next_cursor a través de todas las páginas. Una implementación de polling que no maneja la paginación silenciosamente perderá datos bajo carga.

El patrón híbrido

Para la mayoría de los casos de uso en producción, la mejor respuesta no es una u otra.

Diagrama de arquitectura híbrida: una vez que se publique el evento click.created planificado, Elido empuja eventos de clic a un handler de webhook como ruta principal en tiempo real, mientras un polling nocturno de /summary y /timeseries reconcilia los totales en tu almacén y llena las brechas dejadas por la ventana de reintentos.

Una vez que se publique click.created, usa webhooks como el camino primario para reacción en tiempo real: actualizaciones de CRM, contadores en vivo, workflows event-driven. La latencia es baja; la sobrecarga operacional es manejable si ya tienes infraestructura HTTPS entrante. Hasta entonces, la vía rápida es un polling de intervalo corto a clicks/recent, y la pasada de reconciliación de abajo funciona igual sobre ella.

Usa polling como una pasada de reconciliación semanal o diaria: extrae la serie temporal completa de la semana anterior, compara los totales contra lo que tu handler de webhook registró, e identifica cualquier brecha. Esto captura entregas que agotaron su ventana de reintentos durante una caída, eventos que llegaron fuera de orden, y cualquier discrepancia entre tu estado local y la fuente de verdad de Elido.

La API de analítica está bien adaptada a este rol. El endpoint /summary devuelve totales agregados para un rango de fechas en una sola consulta; el endpoint /timeseries devuelve buckets diarios. Un trabajo de reconciliación que corre una vez por noche y compara los conteos de clics registrados de tu CRM contra el resumen de la API para la misma ventana puede sacar a la luz problemas de integridad de datos antes de que se conviertan en problemas user-facing.

Un cron de polling en Python

El polling es donde comienza hoy toda integración de clics, y seguirá funcionando después de que se publiquen los webhooks por clic. Aquí hay una implementación mínima usando la librería schedule que llama a clicks/recent en un intervalo de cinco minutos (la guía de la API de analítica de enlaces cubre en detalle los parámetros de cursor, fecha y zona horaria):

import schedule
import time
import requests
import os

API_BASE = "https://api.elido.app/v1"
WORKSPACE_ID = os.environ["ELIDO_WORKSPACE_ID"]
API_KEY = os.environ["ELIDO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# Newest click already processed, kept across runs. clicks/recent returns
# newest first and next_cursor pages to older rows, so each run reads from
# the top and stops when it reaches this row.
_last_seen = None

def poll_recent_clicks():
    global _last_seen
    params = {"limit": 100}
    newest = None
    done = False

    while not done:
        resp = requests.get(
            f"{API_BASE}/workspaces/{WORKSPACE_ID}/analytics/clicks/recent",
            headers=HEADERS,
            params=params,
            timeout=10,
        )
        resp.raise_for_status()
        body = resp.json()

        for click in body.get("items", []):
            key = (click["ts"], click["link_id"])
            if key == _last_seen:
                done = True
                break
            if newest is None:
                newest = key
            process_click(click)

        next_cursor = body.get("next_cursor")
        if not next_cursor:
            break
        params["cursor"] = next_cursor

    if newest:
        _last_seen = newest

def process_click(click: dict):
    # Replace with your actual processing logic
    print(f"click: link={click['link_id']} country={click.get('country_code')}")

schedule.every(5).minutes.do(poll_recent_clicks)

if __name__ == "__main__":
    poll_recent_clicks()  # run once on startup to catch up
    while True:
        schedule.run_pending()
        time.sleep(10)

En un despliegue de producción, reemplaza el print con tu sink real - una escritura a base de datos, una llamada a la API del CRM, una publicación de cola de mensajes - y añade manejo de errores con backoff exponencial alrededor de la llamada requests.get.

Filtrado de bots y lo que significa para tu integración

Un detalle que afecta a ambos patrones: la capa de redirección de Elido filtra los clics de bots antes de registrarlos. Las solicitudes de Googlebot, Bingbot, Slackbot, monitores de uptime, curl, librerías de scripting, y User-Agents vacíos no se cuentan como clics y no aparecen en los resultados de la API de analítica, y no producirán eventos click.created cuando estos estén disponibles.

Esto importa porque significa que tu handler de webhook o trabajo de polling está trabajando con conteos de redirecciones humanas, no conteos de solicitudes HTTP crudas. Si estás correlacionando datos de clics de Elido contra métricas server-side - los logs del servidor de tu aplicación, los logs de acceso de una CDN - espera que los números de Elido sean menores. La discrepancia no es un bug; es el filtro de bots eliminando ruido antes de que te llegue.

Para más detalle sobre lo que cubre el filtro de bots y cómo el scorer de sospecha marca tráfico fronterizo, la guía de analítica tiene un desglose completo. Para las propiedades de seguridad del esquema de firma de webhook - incluyendo el formato HMAC, el binding de marca de tiempo, y lo que previene - ve la checklist de seguridad.


La página de precios tiene el desglose de qué tiers de plan incluyen endpoints de webhook y a qué topes de volumen de entrega.

Relacionado en el blog

Prueba Elido

Pega una URL, obtén un enlace corto

Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.

Gratis, sin registro · 2 por día

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

Etiquetas
click tracking
webhooks
url shortener api
link analytics
api integration
event-driven
polling
real-time analytics

Seguir leyendo