Un webhook envía la actividad de tu equipo a una URL de tu propiedad, en el momento en que ocurre. Cada acción que llega a tu Registro de auditoría puede enviarse como un POST JSON firmado a tu propio servidor, a tu propio bot o a una herramienta interna: sin sondeos, sin exportaciones, sin extraer datos del panel.
Una sola regla decide todo en esta página. Un webhook lleva exactamente lo que muestra el Registro de actividad de tu equipo, y nada más. No es un canal paralelo con su propio alcance ni una carga útil más rica para máquinas. Si no está en esa página, no está en el cuerpo.

📍 Dónde encontrarlo
Configuración del equipo → Webhooks, en la página propia de tu equipo. Hasta tres destinos por equipo, cada uno con su propia URL, sus propias categorías y su propio secreto de firma.
Quién puede configurarlo: el propietario del equipo, o un miembro cuyo rol incluya Quitar un servidor del equipo. Es la misma regla que abre el propio Registro de actividad: un webhook reenvía lo que muestra esa página, así que poder leerla es poder reenviarla. Consulta Roles y permisos del equipo.
🎯 Qué se envía y qué nunca se envía
Tú eliges las categorías. Solo se entregan las que marques:
| Categoría | Qué abarca |
|---|---|
| Funciones | Una función activada, desactivada o con sus ajustes guardados |
| Servidores | Un servidor añadido al equipo o quitado de él |
| Equipo | Equipo renombrado, roles editados, webhooks modificados |
| Miembros | Miembros invitados, añadidos, eliminados o que se van |
| Moderación | Sanciones, apelaciones, reportes, tickets, historiales borrados |
| Suscripción | Cambios de suscripción del equipo |
| Facturación | Eventos de facturación del equipo |
| Acciones de la plataforma sobre este equipo | Lo que un administrador de la plataforma hizo en uno de tus servidores |
🚫 Tres cosas que nunca viajan
- Direcciones IP. En el Registro de actividad, una IP se muestra a la persona a quien pertenece y a nadie más. Un webhook no tiene un lector con quien comprobarlo, así que el campo no se envía: se elimina de la carga útil, no se deja en blanco.
- Acciones de backoffice que no tienen que ver contigo. La administración de toda la plataforma no lleva equipo, así que nada de la difusión puede alcanzarla. La categoría Acciones de la plataforma sobre este equipo es solo lo que un administrador hizo en tus servidores.
- Credenciales. Cualquier campo que parezca un token, una clave o un secreto llega como
••••••••, en ambos lados de un par antes/después. Esto se vuelve a aplicar a la salida, de forma independiente al enmascarado que hace la página.
No se ofrecen los inicios de sesión. El registro de auditoría tiene una categoría auth, y falta deliberadamente en la lista anterior: enviar eventos de inicio de sesión a un servidor de terceros, sin supervisión y de forma indefinida, es un acto distinto de mostrarlos en una página que alguien tuvo que abrir. Puede que se añada más adelante.
📦 La entrega
Cada entrega es un único POST con un cuerpo JSON:
{
"version": 1,
"delivery_id": "0a4b2f0e-8c6e-4a5f-9b3d-2f6c1e0d7a11",
"event": "server.feature.enabled",
"category": "feature",
"team": { "id": 12, "name": "Night Shift" },
"data": {
"id": 918273,
"action": "server.feature.enabled",
"category": "feature",
"description": "Enabled Starboard",
"actor_name": "Ava",
"actor_role": "moderator",
"is_admin_action": false,
"team_id": 12,
"team_name": "Night Shift",
"server_id": 4,
"server_name": "Night Shift HQ",
"subject_label": "Starboard",
"changes": { "threshold": { "before": 3, "after": 5 } },
"metadata": null,
"created_at": "2026-09-06T09:41:12+00:00"
}
}
Esos quince campos son todo data: los mismos quince que muestra el Registro de actividad, menos la dirección IP. Cualquiera de ellos puede ser null: una acción sin servidor lleva "server_id": null, una acción sin nada que comparar lleva "changes": null. Léelos a la defensiva en lugar de suponer una forma para cada evento.
versiones la versión del sobre. Existe para que tu receptor pueda ramificarse según ella en lugar de adivinar cuando la forma crezca.eventes la acción auditada. Hay más de ciento treinta y la lista crece con la plataforma: compara por prefijo o porcategory, nunca con una lista exhaustiva de nombres.delivery_ides estable en todos los reintentos del mismo evento. Es lo que hace posible la deduplicación; consulta más abajo.dataes la propia entrada de auditoría, el mismo objeto que muestra el Registro de actividad.
📨 Cabeceras
| Cabecera | Valor |
|---|---|
User-Agent |
YAWBDB-Webhooks/1.0 |
X-YAWBDB-Event |
El nombre de la acción, igual que event en el cuerpo |
X-YAWBDB-Delivery |
El id de la entrega, igual que delivery_id |
X-YAWBDB-Timestamp |
Hora Unix en segundos, de cuando se construyó la petición |
X-YAWBDB-Signature |
v1= seguido del resumen hexadecimal |
🔐 Verificar la firma
Cualquiera que conozca la URL de tu destino puede enviarle un POST con lo que quiera. La firma es lo que te permite distinguir nuestras entregas de las suyas.
El resumen es un HMAC-SHA256 sobre la marca de tiempo, un punto y después el cuerpo sin procesar de la petición, con tu secreto de firma como clave:
signed_string = X-YAWBDB-Timestamp + "." + raw_body
signature = "v1=" + hex( hmac_sha256(signed_string, secret) )
Tres detalles son esenciales, y omitir cualquiera deja un agujero:
- Firma los bytes sin procesar, antes de cualquier análisis del JSON. Volver a serializar el cuerpo lo cambia (orden de las claves, barras escapadas, unicode) y el resumen no coincidirá.
- La marca de tiempo está dentro de la cadena firmada, no solo a su lado. Eso es lo que te permite rechazar repeticiones: rechaza una entrega cuya marca de tiempo tenga más de unos minutos (cinco minutos es una ventana razonable) y un atacante no podrá reenviar una petición que capturó la semana pasada. Como la marca de tiempo está firmada, tampoco puede editarla.
- Compara en tiempo constante. Un simple
==termina en el primer byte distinto, y esa diferencia de tiempo basta para recuperar una firma byte a byte. Usahash_equals,crypto.timingSafeEqual,hmac.compare_digest, como lo llame tu lenguaje.
PHP
$raw = file_get_contents('php://input');
$timestamp = (int) ($_SERVER['HTTP_X_YAWBDB_TIMESTAMP'] ?? 0);
$given = $_SERVER['HTTP_X_YAWBDB_SIGNATURE'] ?? '';
if (abs(time() - $timestamp) > 300) {
http_response_code(400); // too old, or clock is wrong
exit;
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
http_response_code(200); // accepted
Node.js (Express)
import crypto from 'node:crypto';
// The raw body is required - express.json() alone destroys it.
app.post('/yawbdb', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = Number(req.get('X-YAWBDB-Timestamp'));
const given = req.get('X-YAWBDB-Signature') ?? '';
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400);
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${req.body}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(given);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
res.sendStatus(200);
});
El prefijo v1= nombra el esquema. Si algún día cambia, un receptor que compruebe el prefijo puede rechazar deliberadamente uno desconocido en lugar de interpretarlo mal en silencio.
🔑 El secreto
64 caracteres hexadecimales, generados por nosotros, mostrados exactamente una vez: en el panel, justo después de crear el destino o de pulsar Nuevo secreto. Nada lo vuelve a leer después: ni la página, ni la API, ni una exportación. Si lo pierdes, generas uno nuevo.
La rotación se aplica de inmediato a todo lo que se envíe a partir de ese momento. Las entregas ya en cola siguen saliendo firmadas con el secreto antiguo hasta que la cola se vacíe, así que espera un breve solapamiento y actualiza antes tu receptor.
🔁 Entrega, reintentos y deduplicación
- Solo cuenta como recibida una respuesta
2xx. Cualquier otra (3xx,4xx,5xx) es un fallo. Responde rápido y haz el trabajo después. - No se siguen las redirecciones. Un
302es un fallo, no un salto. Danos la URL final. - Los tiempos de espera son cortos: tres segundos para conectar, cinco segundos para responder. Esto es una notificación, no una llamada a procedimiento remoto: confirma primero, procesa después.
- Cuatro intentos, espaciados 30 segundos, 2 minutos y luego 10 minutos: unos doce minutos de principio a fin. Cubre un despliegue, no una caída.
- Los reintentos reutilizan el mismo
delivery_id. Un receptor que trate cadaPOSTcomo nuevo contará doble el día en que nuestro primer intento tenga éxito de tu lado pero agote el tiempo del nuestro. Guarda el id e ignora uno que ya hayas procesado. - No se garantiza el orden. Las entregas se ponen en cola de forma independiente; una reintentada llega después de eventos posteriores. Usa
data.created_atsi el orden te importa.
⛔ Desactivación automática
Tras 20 fallos consecutivos, el destino se desactiva y el panel lo indica en su tarjeta. Un destino que dejó de responder suele haberlo hecho para siempre, y nadie viene a avisarnos: reintentar eternamente costaría una petición y una fila de historial por acción, indefinidamente.
Para reiniciarlo: arregla tu receptor, luego Edita el webhook y marca de nuevo Entregar eventos en este destino. Volver a activarlo es lo que borra el contador, así que un destino reparado no está a un fallo de volver a apagarse.
🧪 Enviar una prueba
Enviar una prueba lanza al instante una petición real y firmada a tu destino y te muestra el código de estado y el tiempo de ida y vuelta. No es una simulación: pasa por el mismo código por el que pasa toda entrega real, con las mismas cabeceras, así que un destino que supera la prueba no puede rechazar después el tráfico de producción por una cabecera que la prueba nunca envió.
El cuerpo de la prueba lleva "event": "panel.webhook.test" y "category": "test", para que tu receptor pueda reconocerla.
Espera dos llegadas, no una. Pulsar el botón es en sí una acción de equipo auditada. Así que tu destino recibe la entrega de prueba y, si está suscrito a la categoría Equipo, una segunda entrega ordinaria de
team.webhook.testedun momento después. Es correcto y no es un bucle.
Una prueba nunca cuenta para el límite de 20 fallos ni lo borra. Depurar un destino no puede desactivarlo.
📜 Intentos recientes
Intentos recientes muestra los últimos 20 intentos de ese destino: cuándo, qué evento, qué número de intento, el código de estado o el error, y cuánto tardó. Los fallos llevan un breve extracto de lo que respondió tu servidor, que suele ser la forma más rápida de descubrir que quien dice que no es un proxy inverso y no tu código.
Los intentos se conservan 30 días y se depuran cada noche. Esto son restos de depuración, no un registro: el propio registro de auditoría tiene una retención propia, mucho más larga.
🛡️ Dónde nos negamos a enviar
La URL de un destino debe apuntar a internet público. Se rechaza una URL que se resuelva a una dirección de loopback, a un rango privado, a una dirección local de enlace o a una dirección de metadatos de la nube: al guardarla, y otra vez antes de cada envío.
La segunda comprobación es la que importa con el tiempo. Un nombre que apuntaba a un lugar público el día que se escribió puede apuntar a 127.0.0.1 un mes después, y un webhook se dispara mientras exista. Un rechazo cuenta como un fallo del destino y no se reintenta: hacerle al resolvedor la misma pregunta otras tres veces no cambiaría nada.
Prefiere un destino https. La firma demuestra quién envió el cuerpo, no lo oculta: con http sin cifrar, tu actividad viaja en claro.
💬 Directamente a un canal de Discord o Slack
Pega una URL de webhook de Discord (https://discord.com/api/webhooks/…) como destino y el panel envía un mensaje de Discord en lugar del sobre JSON: un embed por entrada, con la descripción como título, con el evento, la categoría, el autor, el equipo, el servidor, el asunto y, cuando los hay, los cambios como una breve comparación. La marca de tiempo es la de la entrada, y el pie lleva el id de la entrega.
El embed se construye a partir del mismo sobre descrito arriba, así que hereda sus dos reglas: sin dirección IP y credenciales enmascaradas. Las menciones se desactivan del lado de Discord, así que un canal o rol llamado @everyone en una entrada se publica como texto y no avisa a nadie. Las cabeceras X-YAWBDB-* y la firma se siguen enviando; Discord simplemente las ignora.
Discord responde 204 No Content a un mensaje que aceptó, que es lo que muestra una fila verde en Intentos recientes.
Lo mismo vale para una URL de webhook entrante de Slack (https://hooks.slack.com/services/…): el panel publica un mensaje de Slack por entrada, con la descripción en negrita, luego una línea por dato, los cambios en un bloque de código y la marca de tiempo y el id de la entrega debajo. Se construye a partir del mismo sobre, así que se cumplen las mismas dos reglas, y los tres caracteres que Slack reserva para sus propios comandos (<, >, &) se escapan, así que un nombre escrito por un miembro nunca puede convertirse en @channel o @here. Slack responde 200 ok a un mensaje que aceptó. Las URL de workflow y de trigger de Slack no son mensajes: conservan el sobre JSON, que un workflow puede desmenuzar con sus propias variables.
💡 Qué construye la gente con esto
- Publicar la actividad de tu equipo en un canal de Discord o Slack propio: una URL de webhook de Discord o Slack funciona tal cual, o pon tu propio receptor en medio para filtrar y reformatear.
- Replicar el registro en tu propio almacén de registros y conservarlo más allá de la ventana de retención del panel.
- Avisar a alguien cuando una acción de moderación llega fuera de horas.
- Lanzar una compilación, una sincronización o una copia de seguridad cuando cambia un ajuste de una función.
🔗 Ver también
- Registro de auditoría: qué registra el historial y qué no registra deliberadamente
- Roles y permisos del equipo: quién puede abrir la página
- Server Logs: el registro del lado de Discord, una función distinta que responde a otra pregunta