Para Builders

Webhooks

Un webhook es la puerta por la que tu código entra a Daguito. Cualquier sistema externo —tu backend, un CRM, un cron— puede disparar un flujo con una sola llamada. Hay dos transportes: HTTP para llamadas puntuales y WebSocket para flujos largos en streaming. Cada token se guarda cifrado, cada entrega saliente va firmada y todo queda registrado.

Que es un webhook

Un webhook es un punto de entrada con nombre que apunta a un flujo. Le das un token a un sistema externo y ese sistema dispara el flujo cuando lo necesita. Tu no expones tu API: expones un endpoint estrecho y firmado que solo sabe ejecutar ese flujo.

Eliges el transporte al crear el webhook, según como lo vayas a consumir:

Info

Los tokens nunca se guardan en texto plano. Se almacena un hash SHA-256 para resolverlos en O(1) y el texto plano solo se devuelve una vez, al crear el webhook.

Tipos y transporte

El campo type del webhook decide que endpoint activa su token. Hay dos valores: http y streaming.

Webhooks HTTP

Un webhook HTTP expone dos modos, según cuanto pueda esperar quien llama:

Webhooks de streaming

Un webhook de streaming abre un WebSocket bidireccional en /v1/webhooks/:id/stream. El token viaja como parametro ?token= en la conexión. El cliente maneja una sesión con frames y recibe los eventos del flujo en vivo.

Estas son las tres formas de invocar un webhook, según su tipo:

# HTTP — sincronico (one-shot, el token es la unica auth)
POST https://api.daguito.com/h/dgsk_wh_...

# HTTP — asincronico (202 inmediato, auth Bearer)
POST https://api.daguito.com/v1/webhooks/:id/messages
Authorization: Bearer dgsk_wh_...

# Streaming — WebSocket bidireccional
wss://api.daguito.com/v1/webhooks/:id/stream?token=dgsk_wh_...
Advertencia

El tipo es inmutable: se fija al crear el webhook y no cambia después. Para pasar de HTTP a streaming hay que crear un webhook nuevo.

Tokens y seguridad

Cada webhook tiene un token con el que se autentica. El modelo es simple y estricto:

// Formato y hashing del token
const plaintext = 'dgsk_wh_' + crypto.randomBytes(24).toString('base64url')
const tokenHash = sha256(plaintext)            // solo se guarda el hash
const tokenPreview = 'dgsk_wh_...' + plaintext.slice(-4)  // y una vista previa
Advertencia

No hay rotacion automatica de tokens ni endpoint de borrado. Para invalidar un token cambia el estado del webhook a revoked; para uno nuevo, crea otro webhook.

HTTP sincronico: POST /h/:token

Es la forma mas directa: una sola llamada que bloquea hasta que el flujo termina. Ideal para integraciones que necesitan la respuesta de inmediato.

Desde el SDK, usa runWebhook para esta llamada sincronica:

import { runWebhook } from '@daguito/sdk'

const result = await runWebhook({
  apiUrl: 'https://api.daguito.com',
  token: 'dgsk_wh_...', // token en texto plano
  input: { user_id: '123', action: 'process' },
  timeoutMs: 30000,
})

console.log(result)
// { ok: boolean, executionId: string, status: string, output: unknown }

HTTP asincronico: POST /v1/webhooks/:id/messages

Cuando no quieres bloquear, este modo encola el mensaje y responde al instante. El flujo corre aparte y los resultados llegan por callbacks.

Advertencia

Esta ruta no está pensada para flujos muy largos: procesa la entrega de inmediato. Para procesos de larga duración, conviene dividirlos en pasos más cortos.

Callbacks salientes

Un callback es un POST de servidor a servidor que Daguito envia a una URL tuya cuando ocurre un evento del flujo. Asi recibes resultados sin tener que sondear.

Forma del payload

// Headers del POST saliente
x-daguito-signature: sha256=<hex>    // HMAC-SHA256 del cuerpo, con secret_hash
x-daguito-event:      flow.completed // el tipo de evento
x-daguito-webhook-id: wh_abc123      // id del webhook
Content-Type:         application/json

// Cuerpo
{
  "event": "flow.completed",
  "webhook_id": "wh_abc123",
  "session_key": "webhook:...",
  "flow_id": "...",
  "at": "2026-01-01T12:00:00Z",
  "data": { /* especifico del evento */ }
}

Verificar la firma

Recalcula el HMAC sobre el cuerpo en crudo (sin re-serializar) con tu secreto y comparalo con el header. Si no coincide, rechaza la petición.

import crypto from 'node:crypto'

const expected = crypto
  .createHmac('sha256', webhookSecret)
  .update(rawBody) // el cuerpo en bytes, sin re-serializar
  .digest('hex')

const received = req.headers['x-daguito-signature'] // sha256=<hex>
if (received !== 'sha256=' + expected) {
  throw new Error('signature mismatch')
}
Tip

Cada intento de callback, exitoso o fallido, queda registrado para que puedas depurar tus entregas.

Streaming por WebSocket

Para flujos largos —generaciones de LLM, pipelines de vision, varias herramientas en cadena— el WebSocket evita el limite del proxy y te entrega los eventos a medida que ocurren.

Ciclo de vida de la sesión

El cliente conduce una sesión con frames: session.start crea el session_key, session.open dispara el flujo con el base_input, message envia texto o audio y session.end la cierra. El servidor confirma cada paso y empuja los FlowEvent.

// El cliente envia:
{ "type": "session.start", "session_key": "..." }
{ "type": "session.open", "base_input": { /* ... */ } }
{ "type": "message", "message": { /* normalized message */ } }
{ "type": "tool_result", "call_id": "...", "ok": true, "result": {} }
{ "type": "session.end" }
{ "type": "ping" }

// El servidor responde:
{ "type": "ready", "webhook_id": "wh_abc123" }
{ "type": "session.started", "session_key": "..." }
{ "type": "session.opened", "session_key": "...", "execution_id": "..." }
{ "type": "session.ended" }
{ "type": "pong", "at": "..." }
{ "type": "error", "message": "..." }
// ...y FlowEvent por cada evento que pase el filtro events_downstream

Los eventos que llegan al cliente se filtran con el array events_downstream del webhook: patrones con comodín (por ejemplo flow.* o node.token) compilados una vez por sesión.

El SDK envuelve toda la sesión en runWebhookStream, que no tiene el limite de 100 s del proxy:

import { runWebhookStream } from '@daguito/sdk'

const result = await runWebhookStream({
  apiUrl: 'https://api.daguito.com',
  webhookId: 'wh_abc123',
  token: 'dgsk_wh_...', // token de streaming
  input: { vision_image: '...' },
  timeoutMs: 300000, // 5 minutos — sin limite del proxy
})

console.log(result)
// { ok, executionId, status: 'completed'|'failed'|'unknown', output, elapsedMs }
Advertencia

La reconexión no es automatica. Cada socket se autentica por separado con el token, asi que tu cliente debe reconectar y reabrir la sesión si la conexión se cae.

Eventos de flujo

Tanto los callbacks como el WebSocket entregan FlowEvent: el mismo modelo de eventos que recorre la ejecución de un flujo de principio a fin.

Tipos de evento

El sobre del evento

export type EventType =
  | 'flow.started' | 'flow.resumed' | 'flow.completed'
  | 'flow.failed'  | 'flow.paused'  | 'flow.branch'
  | 'node.started' | 'node.completed' | 'node.progress'
  | 'node.emit'    | 'node.token'
  | 'merge.progress' | 'handoff.requested'

export interface FlowEvent {
  type: EventType
  session_key: string
  flow_id?: string                // uuid
  node_id?: string
  branch_id?: string
  at: string                      // timestamp ISO
  data: Record<string, unknown>   // la forma depende del tipo
}

Filtros con comodín

Tanto events (callbacks) como events_downstream (streaming) aceptan patrones con comodín. El punto separa segmentos y el asterisco actua de comodín:

'flow.completed'   // coincidencia exacta
'flow.*'           // todos los eventos de flow
'node.*'           // todos los eventos de node
'*'                // todos los eventos

Configuración y limites

La configuración del webhook vive en config y depende del tipo. Estos son los limites principales:

export type WebhookType   = 'http' | 'streaming'
export type WebhookStatus = 'active' | 'paused' | 'revoked'

export interface Webhook {
  id: string                 // wh_*
  org_id: string
  flow_id: string            // uuid
  name: string               // 1-200 chars
  type: WebhookType          // fija el transporte
  token_preview: string      // dgsk_wh_...LAST4
  status: WebhookStatus
  config: {
    http?: {
      timeout_ms: number     // 1-60000, default 30000
      outbound_callbacks: OutboundCallback[]
    }
    streaming?: {
      audio_format?: string
      sample_rate?: number
      events_downstream: string[] // requerido, patrones con comodin
    }
    audio?: {
      max_session_seconds?: number
      max_bytes_per_session?: number
      max_concurrent_per_webhook?: number // 0 = sin tope
    }
  }
  last_used_at: string | null
}
Advertencia

En webhooks de streaming, events_downstream es obligatorio y no tiene valor por defecto. Si lo omites, la lista de permitidos queda vacia y el cliente no recibira ningun evento.

Auditoria y observabilidad

Cada entrega saliente se registra para que puedas depurar y reintentar. Daguito guarda un historial de entregas que puedes inspeccionar.

Info

Cada entrega fallida queda registrada con su estado y la hora del próximo reintento. El reintento automático está en nuestra hoja de ruta.

¿Qué sigue?