11 min de lecturaFunciones

Webhooks para eventos de enlaces: payloads, firmas, reintentos

Webhooks de acortador de URL para eventos de enlaces: el envoltorio real del payload, comprobaciones HMAC de X-Webhook-Signature en Node y Python, la política de reintentos y las claves de deduplicación.

Marius Voß
DevRel · edge infra
Diagrama de estilo pixel de los webhooks del acortador de URL: los eventos link.created, link.updated y member.invited pasan a través de una entrega firmada a endpoints de event, siem y discord, bajo una barra que dice HMAC-SHA256 v1=, timeout de 10s, 3 intentos

Los webhooks del acortador de URL de Elido envían mediante POST un envoltorio JSON firmado a tu endpoint HTTPS cada vez que algo cambia en un workspace: se crea, edita, elimina, expira un enlace o alcanza su límite de clics, se invita a un miembro, se verifica un dominio. Cada solicitud lleva una cabecera X-Webhook-Signature: v1=<hex>, que es HMAC-SHA256 sobre {timestamp}.{raw_body}, y una entrega fallida recibe tres intentos en unos veinte minutos.

Lo que no se envía hoy es un webhook de clic en un enlace. Los clics salen a través de la API de analítica y los reenviadores de eventos en su lugar, y te mostraré dónde al final. Este artículo es la mitad saliente de la superficie de la API; la guía rápida de la API + SDKs del acortador de URL cubre la mitad entrante, y smart links explicados es la referencia principal de funciones de donde vienen los eventos de enlaces.

Qué eventos de enlaces disparan un webhook hoy

Cada evento de abajo llega a un endpoint de webhook que se suscribió a él por nombre. El formulario de nuevo endpoint del dashboard ofrece casillas para los ocho más comunes; la API acepta cualquier nombre de la lista.

EventoSe dispara cuandoCasilla del dashboard
link.createdSe crea un enlace, uno a uno o en una importación masivasí
link.updatedCambia el destino, la configuración o el estado, ediciones masivas, restauracionessí
link.deletedSe elimina un enlace, individualmente o en masasí
link.expiredUn enlace pasa su fecha de expiraciónsolo API
link.cap_reachedUn enlace alcanza su número máximo de clicssolo API
link.brokenLa comprobación de enlaces rotos detecta que el destino fallasolo API
workspace.created, workspace.updatedSe aprovisiona un workspace o cambia su configuraciónsí
member.invited, member.removedSe añade un miembro (directamente, por SCIM o por una invitación aceptada) o se eliminasí
member.role_changedCambia el rol de un miembrosolo API
invitation.created, invitation.acceptedSe envía o se acepta una invitaciónsolo API
domain.verified, domain.ssl_failedUn dominio personalizado pasa las comprobaciones DNS, o sigue fallándolas tras 24 horassolo API
audit.eventCualquier entrada del registro de auditoríasí

Los enlaces cifrados añaden dos más, link.encrypted_created y link.encryption_rotated. Si prefieres no mantener la lista actualizada a mano, crea un endpoint siem: recibe todos los eventos del workspace, entradas de auditoría incluidas, sin ningún filtro de suscripción.

En la hoja de ruta, todavía no disponible: click.created (un flujo de clics muestreado), eventos de facturación como billing.subscription_upgraded, y filtros por endpoint como "solo enlaces en la carpeta X". No te suscribas a esos nombres hoy; nada los publica.

Crear un endpoint de webhook con la API

Un endpoint pertenece a un workspace. Lo creas con un POST a /v1/workspaces/{workspace_id}/webhooks:

curl -X POST "https://api.elido.app/v1/workspaces/$WORKSPACE_ID/webhooks" \
  -H "Authorization: Bearer elido_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/elido",
    "events": ["link.created", "link.updated", "link.deleted"],
    "description": "CRM sync",
    "kind": "event"
  }'

La respuesta es 201 con el endpoint y, para los tipos event y siem, un secreto de una sola vez:

{
  "endpoint": {
    "id": 7,
    "workspace_id": 42,
    "url": "https://hooks.example.com/elido",
    "events": ["link.created", "link.updated", "link.deleted"],
    "is_active": true,
    "description": "CRM sync",
    "kind": "event",
    "config": {},
    "created_at": "2026-09-21T09:12:44Z"
  },
  "secret": "whsec_9f2c..."
}

Elido genera el secreto; tú no envías uno. Cópialo ahora, porque ninguna llamada posterior lo devuelve. kind acepta cinco valores. event y siem son las entregas JSON firmadas que cubre este artículo. discord, telegram y sentry reformatean los mismos eventos como un mensaje de chat o un evento de Sentry, se autentican a través de la URL o un token de bot cifrado, y no llevan cabeceras HMAC.

Los permisos cambiaron este mes. Leer endpoints y el registro de entregas necesita workspace.view. Crear, editar, eliminar, rotar un secreto o reenviar una entrega necesita workspace.edit, lo que significa un administrador o propietario. Una clave de API funciona dentro del workspace para el que se emitió y nunca por encima del rol elegido cuando se creó, así que una clave de nivel viewer recibe un 403 en el POST anterior. Referencia completa: la documentación de webhooks.

El envoltorio del payload del webhook

Cada entrega firmada tiene el mismo envoltorio de cuatro campos. data contiene el registro que cambió, así que para los eventos de enlaces es la fila del enlace:

{
  "type": "link.created",
  "workspace_id": 42,
  "data": {
    "id": 91834,
    "workspace_id": 42,
    "domain_id": 3,
    "slug": "spring-sale",
    "destination_url": "https://shop.example.com/spring",
    "title": "Spring sale landing",
    "tags": ["newsletter"],
    "status": "active",
    "expires_at": null,
    "max_clicks": null,
    "redirect_status": 302,
    "created_by_user_id": 17,
    "created_at": "2026-09-21T09:14:02.184311Z"
  },
  "timestamp": "2026-09-21T09:14:02Z"
}

Esa muestra está recortada; el data real lleva cada columna del enlace, incluyendo reglas de segmentación, carpeta, campaña y campos de escaneo. No hay ID de evento ni short_url en el cuerpo, así que construye la URL corta a partir de tu dominio y slug si la necesitas. Los eventos programados envían un objeto más pequeño en su lugar: link.expired tiene link_id, slug y destination_url, y link.cap_reached añade cap y clicks.

Una corrección que deberías conocer si registraste payloads antes de esta semana: los campos secretos ahora se eliminan antes de que un payload salga de Elido. El password_hash de un enlace protegido con contraseña y el token de una invitación solían aparecer en data; ya no lo hacen, ni en entregas nuevas ni en el registro de entregas. Si guardaste payloads antiguos, vale la pena purgar esos datos.

Verificar la cabecera X-Webhook-Signature

Cada solicitud firmada lleva estas cabeceras:

X-Webhook-Signature: v1=5d8f0c3e...
X-Elido-Signature: v1=5d8f0c3e...
X-Webhook-Timestamp: 1790068442
X-Webhook-Event: link.created
X-Webhook-Delivery: 55120
User-Agent: Elido-Webhooks/1.0

Las dos cabeceras de firma tienen el mismo valor. Elido calcula HMAC-SHA256, tal como se define en el RFC 2104, con la cadena whsec_... completa como clave, sobre la marca de tiempo, un punto, y los bytes del cuerpo sin procesar. El resumen hexadecimal recibe un prefijo v1=. No hay ningún campo t= dentro de la cabecera; la marca de tiempo vive en su propia cabecera.

Flujo de verificación de firma: las cabeceras X-Webhook-Signature y X-Webhook-Timestamp más el cuerpo sin procesar y el secreto alimentan un HMAC-SHA256 sobre ts punto body, comparado en tiempo constante como v1= hexadecimal, luego una puerta de frescura de 5 minutos del lado del receptor se divide en procesar el evento o rechazar con 400

En Node, firma los bytes sin procesar, no un objeto reserializado. Express necesita express.raw({ type: "application/json" }) en esta ruta por esa razón:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyElido(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"));

  // During a rotation the old secret signs X-Elido-Signature-Previous.
  return ["x-elido-signature", "x-elido-signature-previous"].some((name) => {
    const got = Buffer.from(headers[name] ?? "");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

La comprobación de longitud importa: timingSafeEqual de Node lanza un error con buffers de tamaños diferentes en lugar de devolver false. La versión en Python con el módulo estándar hmac:

import hashlib
import hmac
import time


def verify_elido(secret: str, headers, raw_body: bytes, tolerance: int = 300) -> bool:
    ts = headers.get("X-Webhook-Timestamp", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
        return False
    digest = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256)
    expected = "v1=" + digest.hexdigest()
    for name in ("X-Elido-Signature", "X-Elido-Signature-Previous"):
        got = headers.get(name)
        if got and hmac.compare_digest(got, expected):
            return True
    return False

Si la comprobación sigue fallando, la guía de verificación de firmas de webhooks incluye una versión en Go, un nodo de código de n8n y las causas habituales de un desajuste. La ventana de cinco minutos es tu comprobación, no la nuestra. Elido pone una marca de tiempo nueva en cada intento, reintentos incluidos, así que un reintento legítimo nunca parece antiguo. Sin la ventana, cualquiera que capturara una solicitud podría repetirla la semana siguiente y la firma seguiría coincidiendo.

La rotación es POST /v1/workspaces/{workspace_id}/webhooks/{id}/rotate-secret, o el botón Rotate en la página del endpoint. Obtienes el nuevo secreto una vez. Durante los siguientes siete días, cada entrega también lleva X-Elido-Signature-Previous, firmada con el secreto anterior, que es por lo que ambas funciones de arriba lo intentan. Despliega el nuevo secreto en cualquier momento de esa semana y nada falla.

La política de reintentos de webhooks

El worker de entrega recoge las entregas pendientes cada pocos segundos, así que un cambio de enlace normalmente te llega en segundos. Cualquier respuesta 2xx marca la entrega como hecha. Un estado que no sea 2xx, un error de red o ninguna respuesta en 10 segundos cuenta como un intento fallido.

Gráfico de barras del calendario de reintentos de webhooks: intento 1 en T+0, intento 2 cinco minutos después de un fallo en T+5m, intento 3 quince minutos después en T+20m, luego la entrega se marca como fallida sin más intentos automáticos hasta que alguien pulsa Retry o llama al endpoint de reintento

Tres intentos por entrega, veinte minutos de principio a fin. Eso es corto a propósito, y sinceramente es más corto de lo que yo elegiría para un receptor detrás de una VPN poco fiable. Una caída de dos horas de tu lado no quedará cubierta por los reintentos automáticos. Lo que la cubre es el registro de entregas: GET /v1/workspaces/{workspace_id}/webhooks/{id}/deliveries lista cada entrega con estado, código HTTP, latencia, número de intentos y próxima hora de reintento, y la página del endpoint muestra las mismas filas con un botón Retry. Retry, o POST .../deliveries/{delivery_id}/retry, rearma una entrega fallida o entregada con un presupuesto fresco de tres intentos y devuelve 202. Una entrega todavía pendiente recibe 409.

Algunos fallos se saltan los reintentos. Un endpoint de Telegram sin su chat_id, o un DSN de Sentry mal formado, se marca como fallido de inmediato, porque repetir la solicitud no arreglará la configuración. Para poner un endpoint fuera de línea sin eliminarlo, envía PUT con "is_active": false; los endpoints en pausa no reciben nuevas entregas.

Si tu manejador hace un trabajo pesado, devuelve 200 primero y encola el job. El límite de diez segundos es donde un manejador lento pero exitoso se convierte en un duplicado, lo que nos lleva a la deduplicación.

¿Estás planeando un receptor para tu equipo? La página de la función de webhooks muestra el lado del dashboard de todo esto.

Idempotencia y orden de los webhooks

Elido entrega al menos una vez. La sección de reintentos mostró una forma en que ocurre un duplicado: tu manejador confirma el trabajo, y luego se pierde la ventana de diez segundos, y Elido lo envía otra vez. Un Retry manual reenvía a propósito.

Ambos casos mantienen el mismo valor de X-Webhook-Delivery, porque es el ID de una entrega a un endpoint, no de un intento. Indexa sobre él:

CREATE TABLE elido_webhook_seen (
  delivery_id BIGINT PRIMARY KEY,
  received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- in the handler, inside the same transaction as your work:
INSERT INTO elido_webhook_seen (delivery_id) VALUES ($1)
ON CONFLICT (delivery_id) DO NOTHING
RETURNING delivery_id;
-- no row back means you've already handled this delivery

Dos endpoints suscritos al mismo evento reciben dos IDs de entrega distintos, así que deduplica por endpoint. En reinicios poco frecuentes de nuestro lado, un evento puede encolarse dos veces como entregas separadas; si una escritura doble haría daño, añade una segunda protección sobre type más data.id más data.updated_at.

No hay ninguna garantía de orden. Las entregas salen primero las más antiguas pendientes, pero un reintento de un evento anterior puede llegar después de uno posterior. Compara data.updated_at contra lo que ya has guardado antes de sobrescribir un enlace, y no te apoyes en el timestamp del envoltorio para el orden: solo tiene precisión de un segundo.

Dónde viven los datos de clics en lugar de un webhook de clics

Esta es la parte que la versión anterior de este artículo se equivocó. Hoy no existe un webhook de click, y click.created está previsto, no disponible. La ruta de redirección se mantiene libre de trabajo síncrono, algo que explica el artículo sobre ingesta de clics de tipo fire-and-forget, y los clics van al almacenamiento de analítica en vez de a la cola de webhooks.

Para datos a nivel de clic ahora mismo, tienes dos rutas:

  1. Reenviadores de eventos. Cada clic en un enlace corto se convierte en un evento del lado del servidor en la herramienta que ya usas: eventos de clic de Mixpanel, eventos de clic de Klaviyo en perfiles, o métricas de redirección de Datadog para dashboards de operaciones.
  2. La API de analítica. GET /v1/analytics/workspaces/{workspace_id}/clicks/recent devuelve clics recientes, y clicks.csv los exporta, así que un job programado puede extraer las filas que necesita.

Cuál encaja depende de la latencia y de dónde terminan los datos; webhooks frente a sondeo para seguimiento de clics recorre esa contrapartida. Y si el flujo muestreado de click.created llega a lanzarse, esta página lo dirá primero.

Lee la referencia principal: smart links explicados.

Relacionado en el blog

Preguntas frecuentes

¿Elido envía un webhook por cada clic en un enlace?

Hoy no. Los webhooks cubren cambios de workspace como link.created, link.updated, link.expired y member.invited. Un evento click.created está en la hoja de ruta como un flujo muestreado. Para datos a nivel de clic ahora mismo, usa la API de analítica o un reenviador de eventos como Mixpanel, Klaviyo o Datadog.

¿Cómo verifico la firma de un webhook de Elido?

Calcula HMAC-SHA256 sobre el valor de X-Webhook-Timestamp, un punto y el cuerpo de la solicitud sin procesar, usando tu secreto whsec_ como clave. Codifícalo en hexadecimal, añade el prefijo v1= y compáralo en tiempo constante con la cabecera X-Webhook-Signature. Rechaza marcas de tiempo de más de cinco minutos de antigüedad.

¿Cuántas veces reintenta Elido un webhook fallido?

Cada entrega recibe tres intentos: uno inmediato, otro cinco minutos después de un fallo y otro quince minutos después de ese. Cualquier estado que no sea 2xx, un error de red o una respuesta más lenta de diez segundos cuenta como fallo. Después del tercero, la entrega se marca como fallida hasta que pulses Retry.

¿Cuál es la diferencia entre X-Webhook-Signature y X-Elido-Signature?

Ninguna salvo el nombre. Ambas cabeceras llevan la misma firma v1=, y X-Webhook-Signature se mantiene para receptores antiguos. Durante una rotación de secreto, Elido también envía X-Elido-Signature-Previous, firmada con el secreto anterior, durante siete días.

¿Cómo evito procesar el mismo webhook dos veces?

Guarda el valor de la cabecera X-Webhook-Delivery y omite cualquier solicitud cuyo valor ya hayas manejado. Identifica una entrega a un endpoint y se mantiene igual a través de reintentos automáticos y reenvíos manuales, así que un índice único sobre él es suficiente.

¿Quién puede crear o eliminar webhooks en un workspace?

Los administradores y propietarios del workspace. Listar endpoints y leer el registro de entregas necesita acceso de vista; crear, editar, eliminar, rotar un secreto o reenviar una entrega necesita el permiso workspace.edit. Las claves de API están limitadas al rol elegido cuando se creó la clave.

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
url shortener webhooks
link click webhook
webhook signature verification
webhook retry policy
webhook idempotency
webhook payload

Seguir leyendo