Ir al contenido

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.

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:

CabeceraValorPara qué
X-SHARA-Signature<hex> HMAC-SHA256 del cuerpo crudoAutentica el origen del evento.
X-SHARA-Timestamp<unix-seconds>Cierra la ventana anti-replay de ±300 s.
X-SHARA-Event-Idid único del evento (evt_…)Idempotencia: deduplica reintentos.
X-SHARA-Event-Typep. ej. run.completedEnruta sin abrir el cuerpo.
Content-Typeapplication/jsonEl cuerpo es JSON UTF-8.

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.

  1. Entra en Ajustes → Webhooks (requiere rol de administración y verificación en dos pasos activa).
  2. Pulsa Nuevo destino e introduce la URL HTTPS de tu endpoint. Solo se admite https://; el http:// plano se rechaza.
  3. Marca los tipos de evento que quieres recibir. Suscríbete solo a los que vayas a procesar.
  4. Guarda y copia el secreto de firma que aparece. No se vuelve a mostrar: si lo pierdes, tendrás que rotarlo.
  5. Envía un evento de prueba (ping) desde el panel y confirma que tu receptor responde 2xx y verifica la firma correctamente.
  6. Activa el destino. A partir de ahí, Shara empieza a entregarte los eventos suscritos en tiempo real.

El evento ping no 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.

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.

typeCuándo se disparaDatos clave
approval.createdUn 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.decidedUna persona aprueba o rechaza esa solicitud desde el panel.approval_id, decision (approved/rejected), decided_by
run.completedUna ejecución de agente termina con éxito.run_id, agent, alias, usage.stu
run.failedUna ejecución termina con error o se detiene por un kill-switch de gasto.run_id, agent, reason
draft.createdUn agente genera un borrador (correo, propuesta, respuesta) a la espera de revisión humana.draft_id, agent, kind, run_id
integration.connectedSe enlaza un conector (Gmail, Slack, CRM…) en el workspace.integration, provider, connected_by

Los kill-switch de 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 un run.failed con reason indicando el ámbito del corte (run · hour · day), útil para alertar a tu equipo.

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:

json
{
  "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
    }
  }
}

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:

json
{
  "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é:

json
{
  "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:

json
{
  "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:

json
{
  "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):

json
{
  "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"
  }
}

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.

javascript
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.

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.

javascript
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);

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 el id ya visto y descarta duplicados en lugar de reprocesarlos.
  • Orden no garantizado. Bajo carga, dos eventos pueden llegar desordenados. Usa created_at si 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 → Webhooks cuando 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.

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.

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ónShara → tu servidorTu sistema → Shara
Quién firmaShara, con tu secreto per-tenantTú, con tu secreto per-tenant
Quién reintentaShara, con backoffTú, el emisor
EndpointTu URL HTTPSPOST /v1/webhooks/inbound/{slug}
Para quéReaccionar a aprobaciones, runs, borradoresAlimentar 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.com con tu caso de uso.

¿Dudas técnicas sobre los webhooks? Escríbenos a api@aiginer.com.