Los webhooks permiten recibir notificaciones HTTP en tiempo real cuando
ocurren eventos en tu workspace. Solo se entregan eventos para agentes y
suscripciones activas.
Requiere permisos webhooks:read para consultas y
webhooks:write para crear, actualizar o eliminar. Todos los
endpoints requieren el header X-Workspace-Id.
Eventos disponibles
Los webhooks pueden suscribirse a los siguientes eventos:
- tool.invoked: Se ejecuta cuando un agente termina de invocar una herramienta y recibe una respuesta.
Eventos de llamadas (Calls)
- call.callStarted: Una llamada de voz comenzó para uno de los agentes del workspace.
- call.callEnded: Una llamada de voz finalizó (incluye URLs de grabación y transcripción).
- call.callMissed: Una llamada no fue contestada antes del timeout.
- call.voicemailReceived: Un interlocutor dejó un mensaje de buzón de voz.
Eventos de WhatsApp
- whatsapp.messageReceived: Un usuario de WhatsApp envió un mensaje entrante al workspace.
- whatsapp.messageDelivered: Un mensaje saliente de WhatsApp llegó al dispositivo del destinatario.
- whatsapp.messageFailed: Un mensaje saliente de WhatsApp falló al entregarse.
- whatsapp.conversationStarted: La plataforma abrió una nueva sesión de conversación de WhatsApp.
- whatsapp.conversationEnded: Una sesión de conversación de WhatsApp existente se cerró.
Seguridad
Todas las entregas de webhook incluyen:
- HTTPS requerido: Solo se aceptan URLs HTTPS.
- Firma HMAC-SHA256: Cada request incluye el header
x-signature con la firma del payload.
- Timestamp: Header
x-timestamp con marca temporal ISO-8601.
- Secreto: El secreto se genera automáticamente (mínimo 16 caracteres) o puedes proveer el tuyo.
Verificación de firmas
Para validar que el webhook proviene de Agents Studio, verifica la firma HMAC-SHA256:
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
timestamp: string,
secret: string,
): boolean {
const message = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(message)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
// Uso en un endpoint Express
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'] as string;
const timestamp = req.headers['x-timestamp'] as string;
const payload = req.body.toString('utf-8');
if (
!verifyWebhookSignature(
payload,
signature,
timestamp,
process.env.WEBHOOK_SECRET!,
)
) {
return res.status(401).send('Invalid signature');
}
// Procesar el evento
const event = JSON.parse(payload);
console.log('Event received:', event.event);
res.status(200).send('OK');
});
Gestión de webhooks
Crear webhook
import { createClient } from '@getsupervisor/agents-studio-sdk';
const client = createClient({
baseUrl: process.env.API_BASE_URL!,
workspaceId: process.env.WORKSPACE_ID!,
apiKey: process.env.API_KEY!,
});
const webhook = await client.webhooks.create({
url: 'https://mi-servidor.com/webhooks/agents-studio',
agentId: '00000000-0000-4000-8000-000000000301', // Opcional: eventos solo de este agente
description: 'Webhook para notificaciones de llamadas',
method: 'POST', // 'GET' o 'POST' (por defecto: 'POST')
isActive: true,
headers: {
'X-Custom-Header': 'valor',
},
// secret: 'mi-secreto-de-16-caracteres-o-mas', // Opcional: se genera automáticamente si se omite
});
console.log('Webhook creado:', webhook.id);
console.log('Secreto (preview):', webhook.secretPreview);
curl -X POST "$API_BASE_URL/v1/webhooks" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mi-servidor.com/webhooks/agents-studio",
"description": "Webhook para notificaciones de llamadas",
"method": "POST",
"isActive": true,
"headers": {
"X-Custom-Header": "valor"
}
}'
Listar webhooks
import { createClient } from '@getsupervisor/agents-studio-sdk';
const client = createClient({
baseUrl: process.env.API_BASE_URL!,
workspaceId: process.env.WORKSPACE_ID!,
apiKey: process.env.API_KEY!,
});
const page1 = await client.webhooks.list({
page: 1,
limit: 20,
sort: '-createdAt',
});
console.log(`Total: ${page1.meta.total} webhooks`);
for (const webhook of page1.data) {
console.log(`- ${webhook.url} (${webhook.isActive ? 'activo' : 'inactivo'})`);
}
// Paginación automática
if (page1.meta.hasNext) {
const page2 = await page1.next();
console.log('Siguiente página:', page2?.meta.page);
}
curl "$API_BASE_URL/v1/webhooks?page=1&limit=20&sort=-createdAt" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID"
Obtener webhook
const webhook = await client.webhooks.get('webhook-id-uuid');
console.log('URL:', webhook.url);
console.log('Método:', webhook.method);
console.log('Entregas exitosas:', webhook.successCount);
console.log('Entregas fallidas:', webhook.failureCount);
console.log('Última entrega:', webhook.lastDeliveryAt);
console.log('Headers personalizados:', webhook.headers);
curl "$API_BASE_URL/v1/webhooks/WEBHOOK_ID" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID"
Deliveries (historial de entregas)
Los deliveries representan cada intento de entrega de un evento a tu endpoint (incluye status, intentos, error y response status).
Listar deliveries de un webhook
const deliveries = await client.webhooks.listDeliveries('WEBHOOK_ID', {
page: 1,
limit: 50,
// Ejemplo: filtrar por fallidos de un evento específico
filter: 'and(eq(status,"failed"),eq(eventKey,"call.callEnded"))',
});
for (const d of deliveries.data) {
console.log(d.id, d.status, d.responseStatus, d.lastError);
}
curl "$API_BASE_URL/v1/webhooks/WEBHOOK_ID/deliveries?page=1&limit=50&filter=and(eq(status,\"failed\"),eq(eventKey,\"call.callEnded\"))" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID"
Obtener un delivery por ID
const delivery = await client.webhooks.getDelivery('WEBHOOK_ID', 'DELIVERY_ID');
console.log('Status:', delivery.status);
console.log('Response status:', delivery.responseStatus);
console.log('Last error:', delivery.lastError);
curl "$API_BASE_URL/v1/webhooks/WEBHOOK_ID/deliveries/DELIVERY_ID" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID"
Actualizar webhook
const updated = await client.webhooks.update('webhook-id-uuid', {
url: 'https://nuevo-servidor.com/webhook',
isActive: false,
method: 'GET',
description: 'Webhook actualizado',
});
console.log('Webhook actualizado:', updated.id);
curl -X PATCH "$API_BASE_URL/v1/webhooks/WEBHOOK_ID" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"isActive": false,
"description": "Webhook actualizado"
}'
Eliminar webhook
await client.webhooks.delete('webhook-id-uuid');
console.log('Webhook eliminado');
curl -X DELETE "$API_BASE_URL/v1/webhooks/WEBHOOK_ID" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID"
Gestión de suscripciones
Crear suscripción a evento
const subscription = await client.webhooks.createSubscription(
'webhook-id-uuid',
{
eventKey: 'call.callEnded',
isActive: true,
},
);
console.log('Suscripción creada:', subscription.id);
console.log('Evento:', subscription.eventKey);
curl -X POST "$API_BASE_URL/v1/webhooks/WEBHOOK_ID/subscriptions" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"eventKey": "call.callEnded",
"isActive": true
}'
Listar suscripciones
const subs = await client.webhooks.listSubscriptions('webhook-id-uuid', {
page: 1,
limit: 50,
});
for (const sub of subs.data) {
console.log(`- ${sub.eventKey} (${sub.isActive ? 'activo' : 'inactivo'})`);
}
curl "$API_BASE_URL/v1/webhooks/WEBHOOK_ID/subscriptions?page=1&limit=50" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID"
Obtener suscripción
const sub = await client.webhooks.getSubscription(
'webhook-id-uuid',
'subscription-id-uuid',
);
console.log('Evento:', sub.eventKey);
console.log('Activa:', sub.isActive);
curl "$API_BASE_URL/v1/webhooks/WEBHOOK_ID/subscriptions/SUBSCRIPTION_ID" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID"
Actualizar suscripción
const updated = await client.webhooks.updateSubscription(
'webhook-id-uuid',
'subscription-id-uuid',
{
isActive: false,
},
);
console.log('Suscripción actualizada');
curl -X PATCH "$API_BASE_URL/v1/webhooks/WEBHOOK_ID/subscriptions/SUBSCRIPTION_ID" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"isActive": false
}'
Eliminar suscripción
await client.webhooks.deleteSubscription(
'webhook-id-uuid',
'subscription-id-uuid',
);
console.log('Suscripción eliminada');
curl -X DELETE "$API_BASE_URL/v1/webhooks/WEBHOOK_ID/subscriptions/SUBSCRIPTION_ID" \
-H "X-API-Key: $API_KEY" \
-H "X-Workspace-Id: $WORKSPACE_ID"
Estructura de payloads
Evento: call.callEnded
Se dispara cuando una llamada de voz finaliza. Incluye información del objetivo alcanzado, URLs de grabación y transcripción.
{
"event": "call.callEnded",
"timestamp": "2025-01-18T10:30:00.000Z",
"workspaceId": "00000000-0000-4000-8000-000000000100",
"agentId": "00000000-0000-4000-8000-000000000301",
"callId": "call-uuid-12345",
"status": "ended",
"sipCode": 180,
"endReason": "dial_no_answer",
"durationMs": 45000,
"result": {
"goal": {
"achieved": true,
"reason": "Customer inquiry resolved successfully"
},
"recordingRoute": "https://storage.example.com/recordings/call-uuid-12345.mp3",
"transcriptionRoute": "https://storage.example.com/transcriptions/call-uuid-12345.json"
},
"metadata": {
"toNumber": "+1234567890",
"createdAt": "2025-01-18T10:29:15.000Z",
"executionId": "execution-uuid-67890"
}
}
Evento: call.callStarted
Se dispara cuando una llamada de voz comienza.
{
"event": "call.callStarted",
"timestamp": "2025-01-18T10:29:15.000Z",
"workspaceId": "00000000-0000-4000-8000-000000000100",
"agentId": "00000000-0000-4000-8000-000000000301",
"callId": "call-uuid-12345",
"status": "started",
"metadata": {
"toNumber": "+1234567890",
"direction": "outbound",
"executionId": "execution-uuid-67890"
}
}
Se dispara cuando un agente termina de ejecutar una herramienta.
{
"event": "tool.invoked",
"timestamp": "2025-01-18T10:30:00.000Z",
"workspaceId": "00000000-0000-4000-8000-000000000100",
"agentId": "00000000-0000-4000-8000-000000000301",
"toolId": "tool-uuid-54321",
"toolIdentifier": "search_knowledge_base",
"executionId": "execution-uuid-67890",
"result": {
"success": true,
"output": "Query results here..."
}
}
Evento: whatsapp.messageReceived
Se dispara cuando se recibe un mensaje entrante de WhatsApp.
{
"event": "whatsapp.messageReceived",
"timestamp": "2025-01-18T10:30:00.000Z",
"workspaceId": "00000000-0000-4000-8000-000000000100",
"agentId": "00000000-0000-4000-8000-000000000301",
"messageId": "wamid.12345",
"from": "+1234567890",
"message": {
"type": "text",
"text": {
"body": "Hola, necesito ayuda"
}
},
"metadata": {
"conversationId": "conversation-uuid-11111"
}
}
Mejores prácticas
Los webhooks pueden ser reenviados en caso de fallos. Asegúrate de que tu endpoint pueda procesar el mismo evento múltiples veces sin efectos secundarios:
const processedEvents = new Set<string>();
app.post('/webhook', async (req, res) => {
const event = req.body;
const eventId = `${event.event}-${event.timestamp}-${event.callId || event.messageId}`;
if (processedEvents.has(eventId)) {
console.log('Event already processed:', eventId);
return res.status(200).send('OK');
}
// Procesar el evento
await processEvent(event);
processedEvents.add(eventId);
res.status(200).send('OK');
});
2. Responder rápidamente
Responde con 2xx lo antes posible y procesa el evento de forma asíncrona:
app.post('/webhook', async (req, res) => {
const event = req.body;
// Responder inmediatamente
res.status(200).send('OK');
// Procesar asíncronamente
setImmediate(async () => {
try {
await processEvent(event);
} catch (error) {
console.error('Error processing event:', error);
}
});
});
3. Reintentos automáticos
El sistema reintenta entregas fallidas hasta 5 veces con backoff exponencial:
- Intento 1: inmediato
- Intento 2: ~1 segundo después
- Intento 3: ~2 segundos después
- Intento 4: ~4 segundos después
- Intento 5: ~8 segundos después
Solo respuestas con código 2xx se consideran exitosas.
4. Monitorear entregas
Usa los campos successCount, failureCount y lastDeliveryAt del webhook para monitorear la salud:
const webhook = await client.webhooks.get('webhook-id');
if (webhook.failureCount > 10) {
console.warn(`Webhook ${webhook.id} has ${webhook.failureCount} failures`);
}
if (webhook.lastDeliveryAt) {
const hoursSinceLastDelivery =
(Date.now() - new Date(webhook.lastDeliveryAt).getTime()) /
(1000 * 60 * 60);
if (hoursSinceLastDelivery > 24) {
console.warn(`No deliveries in ${hoursSinceLastDelivery.toFixed(1)} hours`);
}
}
5. Filtrar por agente
Si solo necesitas eventos de un agente específico, configura agentId al crear el webhook:
const webhook = await client.webhooks.create({
url: 'https://mi-servidor.com/webhook',
agentId: 'agent-uuid-12345', // Solo eventos de este agente
// ...
});