Webhooks
Los webhooks hacen que Shara te avise cuando pasa algo relevante en tu workspace: una aprobación pendiente, un run terminado, un borrador nuevo o un conector recién enlazado. En lugar de consultar la API en bucle, registras una URL HTTPS tuya y Shara le envía un POST firmado por cada evento, en el momento en que ocurre. Toda la infraestructura está alojada en la UE.
Cómo funcionan
Sección titulada «Cómo funcionan»Un webhook es una entrega saliente: cuando se produce un evento, Shara construye un payload JSON, lo firma con HMAC y lo envía por POST a la URL que hayas configurado. Es el patrón inverso al de los webhooks entrantes (donde tus sistemas envían eventos hacia Shara): aquí el tráfico va de Shara hacia ti, sin que tengas que hacer polling de GET /v1/runs.
Los configuras desde el panel en Ajustes → Webhooks. Cada destino tiene: una URL HTTPS de tu propiedad, la lista de tipos de evento que quieres recibir y un secreto de firma propio (per-tenant, nunca compartido entre workspaces) que se muestra una sola vez al crearlo. Guárdalo en tu gestor de secretos: es la única forma de verificar que un evento viene de verdad de Shara.
Cada entrega llega con estas cabeceras. Tu receptor debe leer el cuerpo crudo (los bytes tal cual) antes de parsear el JSON, porque la firma se calcula sobre esos bytes:
| Cabecera | Valor | Para qué |
|---|---|---|
X-SHARA-Signature | <hex> HMAC-SHA256 del cuerpo crudo | Autentica el origen del evento. |
X-SHARA-Timestamp | <unix-seconds> | Cierra la ventana anti-replay de ±300 s. |
X-SHARA-Event-Id | id único del evento (evt_…) | Idempotencia: deduplica reintentos. |
X-SHARA-Event-Type | p. ej. run.completed | Enruta sin abrir el cuerpo. |
Content-Type | application/json | El cuerpo es JSON UTF-8. |
Registrar un webhook paso a paso
Sección titulada «Registrar un webhook paso a paso»Dar de alta un destino es cosa de un minuto desde el panel. El único paso irrepetible es copiar el secreto: se enseña una sola vez.
- Entra en
Ajustes → Webhooks(requiere rol de administración y verificación en dos pasos activa). - Pulsa Nuevo destino e introduce la URL HTTPS de tu endpoint. Solo se admite
https://; elhttp://plano se rechaza. - Marca los tipos de evento que quieres recibir. Suscríbete solo a los que vayas a procesar.
- Guarda y copia el secreto de firma que aparece. No se vuelve a mostrar: si lo pierdes, tendrás que rotarlo.
- Envía un evento de prueba (
ping) desde el panel y confirma que tu receptor responde2xxy verifica la firma correctamente. - Activa el destino. A partir de ahí, Shara empieza a entregarte los eventos suscritos en tiempo real.
El evento
pingno cuenta como actividad de tu workspace ni consume STU: sirve solo para que valides extremo a extremo la URL, la firma y el tiempo de respuesta antes de fiarte del destino.
Eventos disponibles
Sección titulada «Eventos disponibles»Suscríbete solo a los tipos que necesites. Cada evento identifica el agente implicado por su nombre (Amadeus orquesta; los departamentales son Carnegie, Kotler, Graham, Holmes, Maslow, Deming, Rosling, Porter, Turing y Carlzon) y, cuando aplica, el alias del modelo usado (Symphony · Sonata · Prelude · Concerto) y el consumo en STU.
type | Cuándo se dispara | Datos clave |
|---|---|---|
approval.created | Un agente solicita luz verde antes de ejecutar una acción sensible (enviar un email, mover un registro del CRM). | approval_id, agent, action, expires_at |
approval.decided | Una persona aprueba o rechaza esa solicitud desde el panel. | approval_id, decision (approved/rejected), decided_by |
run.completed | Una ejecución de agente termina con éxito. | run_id, agent, alias, usage.stu |
run.failed | Una ejecución termina con error o se detiene por un kill-switch de gasto. | run_id, agent, reason |
draft.created | Un agente genera un borrador (correo, propuesta, respuesta) a la espera de revisión humana. | draft_id, agent, kind, run_id |
integration.connected | Se enlaza un conector (Gmail, Slack, CRM…) en el workspace. | integration, provider, connected_by |
Los
kill-switchde seguridad cortan el gasto en 5 € por run, 50 € por hora y 400 € por día. Cuando uno de ellos detiene una ejecución, recibirás unrun.failedconreasonindicando el ámbito del corte (run·hour·day), útil para alertar a tu equipo.
Estructura del evento
Sección titulada «Estructura del evento»Todos los eventos comparten un sobre común: un id único, el type, la marca temporal created_at, el tenant_id de tu workspace y un objeto data cuya forma depende del tipo. Los textos generados por agentes ya vienen redactados (nunca material crudo de proveedores). Ejemplo de un run.completed:
{
"id": "evt_7c4a91f2",
"type": "run.completed",
"created_at": "2026-07-10T09:42:15Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"run_id": "run_a1b2c3",
"agent": "carnegie",
"alias": "Sonata",
"status": "completed",
"usage": {
"input_tokens": 184,
"output_tokens": 412,
"stu": 1280
}
}
}
Ejemplos de payload por tipo
Sección titulada «Ejemplos de payload por tipo»El sobre es siempre el mismo; lo que cambia es data. Estos son los cuerpos que recibirás para los tipos más habituales. Una aprobación pendiente (approval.created), pensada para avisar a la persona que debe dar el visto bueno:
{
"id": "evt_4Lm9Zx01",
"type": "approval.created",
"created_at": "2026-07-10T10:05:33Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"approval_id": "apr_4Lm9Zx",
"run_id": "run_a1b2c3",
"agent": "carnegie",
"action": "email.send",
"summary": "Email de seguimiento a 3 clientes con incidencias abiertas.",
"expires_at": "2026-07-11T10:05:33Z"
}
}
La resolución de esa aprobación (approval.decided), con quién decidió y qué:
{
"id": "evt_4Lm9Zx02",
"type": "approval.decided",
"created_at": "2026-07-10T10:18:11Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"approval_id": "apr_4Lm9Zx",
"decision": "approved",
"decided_by": "usr_kv71",
"run_id": "run_a1b2c3"
}
}
Un borrador nuevo (draft.created) a la espera de revisión humana:
{
"id": "evt_8b1d0af5",
"type": "draft.created",
"created_at": "2026-07-10T10:06:02Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"draft_id": "drf_9k2p",
"run_id": "run_a1b2c3",
"agent": "carnegie",
"kind": "email",
"subject": "Seguimiento de tus incidencias de esta semana"
}
}
Un run detenido (run.failed). Aquí lo paró un corte de gasto diario; reason indica el ámbito para que alertes al equipo:
{
"id": "evt_c3d4e5f6",
"type": "run.failed",
"created_at": "2026-07-10T11:41:20Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"run_id": "run_z9y8x7",
"agent": "porter",
"reason": "kill_switch:day",
"status": "failed"
}
}
Y un conector recién enlazado (integration.connected):
{
"id": "evt_a1a2a3a4",
"type": "integration.connected",
"created_at": "2026-07-10T12:00:00Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"integration": "gmail",
"provider": "google",
"connected_by": "usr_kv71"
}
}
Verificación de firma HMAC
Sección titulada «Verificación de firma HMAC»La firma es HMAC-SHA256 calculada sobre el cuerpo crudo en bytes con el secreto per-tenant del webhook, y llega en hex minúsculas (64 caracteres) en X-SHARA-Signature. Verifica siempre antes de procesar: (1) comprueba que X-SHARA-Timestamp cae dentro de la ventana anti-replay de ±300 s respecto a tu reloj; (2) recalcula el HMAC sobre los bytes exactos; (3) compara en tiempo constante (timingSafeEqual) para no filtrar información por temporización. Si algo no cuadra, descarta el evento con un 401.
import { createHmac, timingSafeEqual } from 'node:crypto';
// secreto de firma per-tenant (Ajustes → Webhooks); nunca compartido entre workspaces
const SECRET = process.env.SHARA_WEBHOOK_SECRET;
// rawBody = Buffer con los bytes EXACTOS recibidos (no el JSON re-serializado)
export function verifySharaEvent(rawBody, headers) {
const signature = headers['x-shara-signature'] ?? '';
const timestamp = Number(headers['x-shara-timestamp'] ?? 0);
// 1) Ventana anti-replay ±300 s, ANTES de calcular el HMAC
const skewSec = Math.floor(Date.now() / 1000) - timestamp;
if (!Number.isFinite(timestamp) || Math.abs(skewSec) > 300) return false;
// 2) HMAC-SHA256 sobre el cuerpo crudo
const expected = createHmac('sha256', SECRET).update(rawBody).digest();
const received = Buffer.from(signature, 'hex');
// 3) Comparación en tiempo constante (protege contra timing attacks)
return received.length === expected.length && timingSafeEqual(received, expected);
}
El mismo esquema de firma (HMAC-SHA256 sobre el cuerpo crudo, hex minúsculas, ventana ±300 s, comparación en tiempo constante) rige en los webhooks entrantes que tú firmas hacia Shara. Aprende uno y sabes los dos.
Un receptor completo
Sección titulada «Un receptor completo»Servidor mínimo en Express que recibe la entrega, verifica firma y ventana, deduplica por X-SHARA-Event-Id y responde rápido delegando el trabajo pesado. Fíjate en el express.raw: necesitas el cuerpo crudo para que el HMAC cuadre byte a byte.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.SHARA_WEBHOOK_SECRET;
const seen = new Set(); // en producción, usa Redis o tu base de datos
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/hooks/shara', (req, res) => {
const sig = req.get('X-SHARA-Signature') ?? '';
const ts = Number(req.get('X-SHARA-Timestamp') ?? 0);
const eventId = req.get('X-SHARA-Event-Id') ?? '';
// 1) Ventana anti-replay ±300 s
const skew = Math.floor(Date.now() / 1000) - ts;
if (!Number.isFinite(ts) || Math.abs(skew) > 300) {
return res.status(401).json({ error: 'timestamp_out_of_window' });
}
// 2) Firma HMAC en tiempo constante
const expected = createHmac('sha256', SECRET).update(req.body).digest();
const received = Buffer.from(sig, 'hex');
const ok = received.length === expected.length && timingSafeEqual(received, expected);
if (!ok) return res.status(401).json({ error: 'signature_invalid' });
// 3) Idempotencia: descarta el duplicado, pero responde 2xx igual
if (eventId && seen.has(eventId)) return res.status(200).json({ ok: true, duplicate: true });
if (eventId) seen.add(eventId);
// 4) Responde YA; procesa el trabajo pesado de forma asíncrona
const event = JSON.parse(req.body.toString('utf8'));
res.status(200).json({ ok: true });
queueForProcessing(event); // tu cola / worker
function queueForProcessing(e) {
console.log('evento verificado:', e.type, e.id);
}
});
app.listen(8080);
Reintentos, idempotencia y errores
Sección titulada «Reintentos, idempotencia y errores»Responde con un 2xx en pocos segundos (idealmente < 10 s) y procesa el trabajo pesado de forma asíncrona. Reglas de entrega:
- Reintentos con backoff. Si tu endpoint responde
5xx, agota el tiempo o no es alcanzable, Shara reintenta con espera exponencial creciente a lo largo de varias horas antes de darse por vencido. - Idempotencia por
X-SHARA-Event-Id. El mismo evento puede llegarte más de una vez (por un reintento tras un timeout en el que sí lo procesaste). Registra elidya visto y descarta duplicados en lugar de reprocesarlos. - Orden no garantizado. Bajo carga, dos eventos pueden llegar desordenados. Usa
created_atsi el orden importa; no asumas secuencia estricta. - Auto-pausa. Tras muchos fallos consecutivos, Shara pausa el destino y lo marca en el panel; reactívalo desde
Ajustes → Webhookscuando tu receptor vuelva a estar sano. - Rotación del secreto. Puedes rotar el secreto de firma en cualquier momento; despliega el nuevo en tu receptor y verifica con él las entregas siguientes.
Buenas prácticas de seguridad
Sección titulada «Buenas prácticas de seguridad»Un webhook es una puerta que abres en tu infraestructura hacia internet. Trátala como tal:
- Verifica la firma siempre, y falla cerrado. Sin firma válida no hay procesamiento. Nunca aceptes un evento «porque el JSON tiene buena pinta».
- Lee el cuerpo crudo. Calcula el HMAC sobre los bytes recibidos, no sobre un JSON re-serializado por tu framework, o la firma no cuadrará.
- Compara en tiempo constante. Usa
timingSafeEqual(o equivalente) para no filtrar la firma correcta por diferencias de temporización. - Guarda el secreto en un gestor de secretos. Nunca en el repositorio, en el frontend ni en logs. Es una credencial de servidor.
- Sirve el endpoint solo por HTTPS. El tráfico va cifrado y Shara no entrega a
http://plano. - Respeta la ventana anti-replay. Rechaza timestamps fuera de ±300 s: es tu defensa contra reenvíos maliciosos de un evento capturado.
- Valida el esquema del payload. Aunque venga firmado, valida los campos antes de actuar; no confíes ciegamente en tipos ni tamaños.
- Responde rápido y procesa aparte. Encola el trabajo y devuelve
2xx; así evitas timeouts que disparan reintentos innecesarios. - Rota el secreto periódicamente y de inmediato ante cualquier sospecha de fuga.
- Monitoriza la auto-pausa. Si Shara pausa el destino por fallos, tu integración está caída: vigílalo en el panel.
Entrantes y salientes: no los confundas
Sección titulada «Entrantes y salientes: no los confundas»Shara tiene dos mecanismos de webhook con nombres parecidos pero sentidos opuestos. Esta página cubre los salientes (Shara te avisa a ti). Los entrantes (tú envías eventos a Shara, por ejemplo un lead de un formulario) se documentan en la API REST.
| Salientes (esta página) | Entrantes (API REST) | |
|---|---|---|
| Dirección | Shara → tu servidor | Tu sistema → Shara |
| Quién firma | Shara, con tu secreto per-tenant | Tú, con tu secreto per-tenant |
| Quién reintenta | Shara, con backoff | Tú, el emisor |
| Endpoint | Tu URL HTTPS | POST /v1/webhooks/inbound/{slug} |
| Para qué | Reaccionar a aprobaciones, runs, borradores | Alimentar a un agente desde un sistema externo |
¿Necesitas un tipo de evento que no está en la lista, o una entrega a un destino especial? Escríbenos a
soporte@aiginer.comcon tu caso de uso.