Usa webhooks cuando una persona o un flujo de trabajo deba reaccionar en segundos, y haz polling cuando unos minutos de retraso no importen y prefieras no mantener un endpoint público. En Elido la elección tiene una pega: los webhooks de seguimiento de clics todavía no existen. Los webhooks cubren eventos de enlaces, dominios, miembros y espacios de trabajo, y un evento click.created por clic está previsto pero no se emite. Hoy los datos de clics salen del polling de la API de analíticas.
Este artículo hace las cuentas del retraso y del volumen de peticiones, cubre lo que garantiza cada patrón (reintentos, duplicados, orden, firmas) y termina con una tabla de decisión. El código se ejecutó en Node 22 contra mocks locales antes de incluirlo. Para el catálogo completo de eventos de enlaces, lee webhooks para eventos de enlaces; para los endpoints de clics, la guía de la API de analíticas de enlaces.
Webhooks vs polling en un párrafo cada uno
Polling significa que tu código pide datos nuevos según un horario. Una tarea cron llama a GET /v1/workspaces/{workspace_id}/analytics/clicks/recent, procesa lo nuevo y se duerme. Tú inicias cada petición. No hace falta una URL pública.
Los webhooks invierten la dirección. Elido envía un POST firmado con un sobre JSON a tu endpoint HTTPS cuando se dispara un evento, y una respuesta 2xx marca la entrega como hecha. Necesitas un endpoint accesible. TLS. Un manejador que aguante los duplicados. Hoy los eventos son link.created, link.updated, link.expired, domain.verified y el resto de la lista de la documentación de webhooks.
La aritmética de latencia y coste
El retraso del polling lo fija el intervalo. Con un intervalo de I minutos, un evento espera I/2 minutos de media y I minutos en el peor caso, más el retraso que tengan los datos antes de poder consultarse. Las peticiones por día son 1.440 / I.
- 1 minuto: 1.440 peticiones al día, 30 segundos de retraso medio, 1 minuto en el peor caso.
- 5 minutos: 288 peticiones, 2,5 minutos de media, 5 minutos en el peor caso.
- 15 minutos: 96 peticiones, 7,5 minutos de media, 15 minutos en el peor caso.
- 60 minutos: 24 peticiones, 30 minutos de media, 60 minutos en el peor caso.
El coste aparece como peticiones desperdiciadas, no como presión sobre los límites de tasa. El límite publicado de espacio de trabajo en Free es de 10 peticiones por segundo, es decir 864.000 al día (consulta límites de tasa de la API e idempotencia). Un poller con intervalo de 1 minuto usa 1.440 / 864.000 = 0,17 % de eso. Chocarás con los límites cuando hagas polling por enlace en cientos de enlaces, o cuando otra automatización comparta el espacio de trabajo, no con un solo feed.
El desperdicio es la cifra que importa. Supón que un espacio de trabajo cambia 40 enlaces al día y los vigilas con polling cada minuto. Como máximo 40 de las 1.440 peticiones devuelven algo nuevo, así que al menos 1.400 (97 %) están vacías. Un receptor de webhooks recibe 40 peticiones para esos mismos 40 eventos. En el peor caso, con tres intentos cada uno, son 120.
Los webhooks ganan en retraso, que suele ser de segundos porque el worker de entregas recoge continuamente las filas pendientes. Pierden en lo que el polling obtiene gratis: una ejecución perdida se repara con la siguiente, porque los datos siguen en la API. Un webhook perdido solo se repara si construiste algo que lo repare.
Garantías de entrega: qué promete Elido
Estos comportamientos salen del código de entrega y de los documentos de ayuda, comprobados el 1 de octubre de 2026.
Reintentos. Una entrega tiene tres intentos: uno al instante, otro 5 minutos después de un fallo y otro 15 minutos después de ese, unos 20 minutos de principio a fin. Cualquier estado distinto de 2xx, error de red o respuesta de más de 10 segundos es un fallo. Tras el tercero, la entrega se marca como fallida; Retry en el panel o POST .../deliveries/{delivery_id}/retry la rearma con un presupuesto nuevo de tres intentos. A modo de comparación, Stripe reintenta los eventos en modo live hasta tres días. Veinte minutos es poco. No des por hecho que se cubrirá una caída de tu receptor.
Idempotencia. La entrega es de al menos una vez. La cabecera X-Webhook-Delivery identifica una entrega a un endpoint y no cambia entre reintentos automáticos o manuales, así que es tu clave de deduplicación.
Orden. No hay promesa de orden. El reintento de un evento anterior puede llegar después de uno posterior, así que compara data.updated_at con lo que guardaste antes de sobrescribir nada.
Firmas. Cada entrega firmada lleva X-Webhook-Signature (alias X-Elido-Signature) con v1= más el HMAC-SHA256 en hexadecimal de {timestamp}.{raw_body}, y la marca de tiempo en X-Webhook-Timestamp. La marca de tiempo es nueva en cada intento, así que un reintento legítimo nunca está caducado. Durante una rotación de secreto, X-Elido-Signature-Previous lleva durante siete días una firma del secreto antiguo. Verificar firmas de webhooks cubre Python, Go y la depuración de una discrepancia.
Transporte y pausa. Los endpoints de eventos nuevos deben usar https; http plano solo se acepta para localhost y 127.0.0.1. Poner is_active en false pausa un endpoint. Desde la corrección del 22 de septiembre, eso detiene también las entregas en cola.
Un receptor de webhooks en Node
Este receptor verifica la firma sobre los bytes en bruto, impone una ventana de cinco minutos, acepta la cabecera del secreto anterior durante la rotación y deduplica por el ID de entrega. La tolerancia de cinco minutos es decisión tuya, no de Elido, y coincide con el valor por defecto de Stripe. Lo ejecuté contra un firmador local: una petición válida devolvió 200, un reenvío con el mismo ID de entrega devolvió "duplicate", y un secreto erróneo y una marca de tiempo de 400 segundos de antigüedad devolvieron 401.
import http from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.WEBHOOK_SECRET; // the whsec_... value
const seen = new Set(); // use a unique index in a real database
export function verify(secret, headers, rawBody, toleranceSec = 300) {
const ts = headers["x-webhook-timestamp"] ?? "";
if (!/^\d+$/.test(ts)) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;
const mac = createHmac("sha256", secret).update(`${ts}.`).update(rawBody);
const expected = Buffer.from("v1=" + mac.digest("hex"));
return ["x-elido-signature", "x-elido-signature-previous"].some((name) => {
const got = Buffer.from(headers[name] ?? "");
return got.length === expected.length && timingSafeEqual(got, expected);
});
}
http
.createServer((req, res) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const raw = Buffer.concat(chunks);
if (req.method !== "POST" || !verify(SECRET, req.headers, raw)) {
return res.writeHead(401).end();
}
const id = req.headers["x-webhook-delivery"];
if (seen.has(id)) return res.writeHead(200).end("duplicate");
seen.add(id);
const event = JSON.parse(raw.toString("utf8"));
queueMicrotask(() => console.log("handled", event.type)); // do the work after replying
res.writeHead(200).end("ok");
});
})
.listen(8787);
La comprobación de longitud antes de timingSafeEqual importa. Node lanza una excepción con buffers de distinto tamaño. Responde primero, trabaja después. El corte de 10 segundos es donde un manejador lento pero correcto se convierte en un duplicado.
Un poller de clics en Node
Hasta que se publique click.created, esta es la vía de los clics. clicks/recent devuelve primero lo más reciente, acepta limit hasta 500 (100 por defecto) y devuelve next_cursor siempre que una página llega llena. Cada ejecución lee desde arriba y se detiene al llegar al clic más reciente que ya procesó. Lo ejecuté contra un mock que pagina de dos en dos filas: la primera ejecución procesó 5 clics, la segunda solo los 2 nuevos y la tercera ninguno.
const BASE = "https://api.elido.app";
const WS = process.env.ELIDO_WORKSPACE_ID;
const KEY = process.env.ELIDO_API_KEY;
// Newest click already handled; clicks/recent is newest first.
let lastSeen = null;
const keyOf = (c) => `${c.ts}|${c.link_id}`;
export async function pollOnce(handle) {
let cursor = null;
let newest = null;
for (;;) {
const url = new URL(`${BASE}/v1/workspaces/${WS}/analytics/clicks/recent`);
url.searchParams.set("limit", "500");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new Error(`clicks/recent returned ${res.status}`);
const { items, next_cursor } = await res.json();
for (const click of items) {
if (keyOf(click) === lastSeen) {
if (newest) lastSeen = newest;
return;
}
newest ??= keyOf(click);
await handle(click);
}
if (!next_cursor) break;
cursor = next_cursor;
}
if (newest) lastSeen = newest;
}
// setInterval(() => pollOnce(handleClick).catch(console.error), 60_000);
Dos límites, dichos con claridad. El cursor ordena por marca de tiempo e ID de enlace, así que dos clics en un mismo enlace en el mismo segundo comparten clave; si no puedes permitirte un clic omitido o repetido en raras ocasiones, haz handle idempotente sobre la fila completa. Y lastSeen vive en memoria, así que persístelo entre reinicios, o un reinicio repetirá el historial reciente.
¿No estás listo para construir ninguno? No pasa nada. Abre el panel y usa primero las analíticas del panel, y añade la API cuando sepas qué números necesitas de forma programada.
Tabla de decisión
| Situación | Qué usar hoy | Por qué |
|---|---|---|
| Reaccionar en segundos a cambios de enlaces o dominios | Webhook en link.*, domain.* | Los eventos se envían; no hace falta un evento de clic |
| Contador de clics en vivo en tu producto | Polling de 1 minuto de clicks/recent | Todavía no hay webhook de clic; 1.440 peticiones al día queda muy dentro del límite |
| Enriquecimiento de CRM por clic | Reenviador de eventos o polling corto | Un retraso de polling de 30 segundos de media suele ser aceptable |
| Informes semanales o mensuales | Polling de summary y timeseries | Los datos retrospectivos no ganan nada con el push |
| No hay endpoint público disponible | Polling | Los webhooks necesitan una URL https accesible desde internet |
| No puedes perder un evento de enlace | Webhook más conciliación nocturna | Los reintentos cubren unos 20 minutos, el polling cubre el resto |
Cuando se publique el flujo muestreado de click.created, las filas del contador en vivo y del CRM pasarán a la columna de webhook, y el polling se convertirá en la pasada de conciliación. Hasta entonces, los reenviadores de eventos como eventos de clic de enlaces en Mixpanel son la opción tipo push para los datos de clics.
El patrón híbrido
Para todo lo que no puedas permitirte perder, ejecuta ambos. El webhook es la vía rápida. Un polling programado compara totales y encuentra huecos: entregas que agotaron sus tres intentos durante una caída, o eventos que llegaron desordenados.
Los informes summary y timeseries devuelven totales y agrupaciones diarias de un rango de fechas en una sola llamada, lo que abarata una comparación nocturna. Recuerda que el filtro de bots elimina rastreadores y monitores antes de registrar los clics, así que tus totales no coincidirán con los registros brutos del servidor; la guía de analíticas explica qué se excluye. La lista de comprobación de seguridad cubre el resto. Si diseñas tus propios webhooks, la especificación Standard Webhooks es una referencia útil.
La pantalla de documentación de la API de un espacio de trabajo DEMO con datos de ejemplo; no es una cuenta de cliente.
Lista de comprobación previa al lanzamiento para cualquiera de los patrones
Antes de lanzar un receptor o un poller, repasa seis comprobaciones. La mayoría de los incidentes de producción con ambos patrones vienen de saltarse una de ellas.
- Webhook: responde con 2xx en menos de 10 segundos y haz el trabajo lento después de responder.
- Webhook: verifica la firma sobre el cuerpo en bruto antes de analizar el JSON.
- Webhook: guarda
X-Webhook-Deliverycon un índice único para que un reintento no pueda contar doble. - Poller: persiste el cursor o la última clave vista fuera de la memoria del proceso.
- Poller: aplica retroceso ante un 429 o 5xx en lugar de reintentar con el intervalo normal.
- Ambos: alerta cuando no haya llegado ningún evento o clic nuevo durante más que tu periodo normal de calma, porque el silencio es el aspecto que tiene una integración rota. Textos independientes como la guía de webhook vs API polling de Hookdeck llegan al mismo consejo: push para la velocidad, polling para conciliar.
Relacionado en el blog
- Webhooks para eventos de enlaces: cargas, firmas, reintentos
- Verificar firmas de webhooks: HMAC-SHA256 en Node, Python, Go
- Límites de tasa de la API de un acortador de URL, reintentos e idempotencia
- API de analíticas de enlaces: obtén estadísticas de clics con una clave de API
- Ingesta de clics fire-and-forget
- Guía rápida de la API y los SDK de un acortador de URL
Preguntas frecuentes
¿Cuál es la diferencia entre webhooks y polling?
Con polling, tu código pide datos nuevos a la API según un horario. Con webhooks, el servidor envía un HTTP POST a tu endpoint cuando algo ocurre. El polling cuesta peticiones y añade hasta un intervalo de retraso. Los webhooks cuestan un endpoint público siempre activo y el trabajo de gestionar reintentos y duplicados.
¿Tiene Elido webhooks para el seguimiento de clics?
Todavía no. Los webhooks cubren eventos de enlaces, dominios, miembros y espacios de trabajo, como link.created y link.expired. Está previsto un evento click.created, pero hoy nada lo publica. Para datos a nivel de clic, consulta por polling el informe clicks/recent de la API de analíticas con su cursor, o usa un reenviador de eventos.
¿Cada cuánto debo consultar por polling una API de analíticas?
Ajusta el intervalo a la decisión que alimentan los datos. Un minuto sirve para un contador casi en vivo, de cinco a quince minutos para paneles, y cada hora o cada día para informes. Un polling de 1 minuto hace 1.440 peticiones al día y uno de 5 minutos hace 288, así que rara vez compensa acortar más.
¿Se garantiza que los webhooks lleguen exactamente una vez?
No. La entrega de webhooks es de al menos una vez, así que un receptor ve duplicados tras un timeout o un reenvío manual. Elido mantiene el mismo valor de X-Webhook-Delivery entre reintentos, así que guarda ese ID y omite todo lo que ya hayas procesado.
¿Qué pasa si mi endpoint de webhooks está caído?
Elido hace tres intentos: uno inmediato, otro cinco minutos después de un fallo y otro quince minutos después de ese. Si los tres fallan, la entrega se marca como fallida hasta que alguien pulse Retry o llame al endpoint de reintento. Las caídas de más de unos veinte minutos necesitan un trabajo de conciliación.
¿Puedo combinar webhooks y polling?
Sí, y para todo lo que importe deberías hacerlo. Usa el webhook como vía rápida y un polling programado como red de seguridad que encuentre los eventos que tu receptor perdió. El polling puede ejecutarse cada hora o cada noche porque solo tiene que cerrar huecos.
Prueba Elido
Pega una URL, obtén un enlace corto
Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.
Sin registro. Crea una cuenta gratis y obtén 50 enlaces al mes.