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:
- HTTP — para integraciones request/response. Llamas, esperas, recibes la salida.
- Streaming — un canal WebSocket bidireccional para flujos largos y eventos en vivo.
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:
- Sincronico —
POST /h/:token. El token es la única autenticación. Bloquea hasta que el flujo termina y devuelve la salida en la respuesta. - Asincronico —
POST /v1/webhooks/:id/messagescon auth Bearer. Responde 202 al instante y el flujo corre en segundo plano; los resultados llegan por callbacks.
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_... 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 — prefijo
dgsk_wh_seguido de 24 bytes aleatorios codificados en base64url. El prefijo antiguosk_wh_también se acepta. - Almacenamiento — se guarda un hash SHA-256, nunca el token en texto plano. El texto plano no persiste tras la creación.
- Vista previa — en el panel ves
dgsk_wh_...LAST4, con los ultimos 4 caracteres, suficiente para reconocerlo sin exponerlo. - Una sola vez — el texto plano se devuelve en la respuesta al crear el webhook y no se puede recuperar después. Guárdalo en tu gestor de secretos.
// 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 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.
- Petición — el cuerpo es JSON libre y se entrega al flujo como
base_input. - Respuesta —
{ ok, execution_id, status, output }. - Timeout — el proxy del edge corta a los 100 segundos. Para flujos mas largos usa el transporte de streaming.
- Facturación — la ruta valida la situación de facturación de la organización y responde 402 si no esta al dia.
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.
- Petición —
{ session_key, message, base_input? }, donde message es el objeto de mensaje normalizado. - Respuesta — 202 inmediato con
{ ok, status, message_id, session_key }. - Auth — token Bearer leido del header
Authorization. - Cuando usarlo — integraciones de alto volumen que necesitan latencia baja y no pueden esperar el resultado.
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.
- Filtro — cada callback declara un array
eventscon patrones (por ejemploflow.completedonode.*). Solo esos eventos disparan el POST. - Firma — el cuerpo va firmado con HMAC-SHA256 usando
secret_hash, en el headerx-daguito-signature: sha256=<hex>. - Headers — ademas viajan
x-daguito-event(tipo) yx-daguito-webhook-id(id del webhook). - Payload —
{ event, webhook_id, session_key, flow_id, at, data }, donde data depende del evento.
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')
} 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 } 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
- Ciclo del flujo —
flow.started,flow.resumed,flow.completed,flow.failed,flow.paused,flow.branch. - Nodos —
node.started,node.completed,node.progress,node.emit,node.token. - Sincronizacion y control —
merge.progressyhandoff.requested.
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:
- http.timeout_ms — entre 1 y 60000 ms, por defecto 30000.
- streaming.events_downstream — requerido en webhooks de streaming; define que eventos llegan al cliente.
- audio —
max_session_seconds,max_bytes_per_sessionymax_concurrent_per_webhook(0 = sin tope). - name — entre 1 y 200 caracteres.
- URL de callback — URL valida, maximo 2048 caracteres; el array events necesita al menos una entrada.
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
} 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.
- Solo agregar — la tabla nunca se actualiza; cada reintento inserta una fila nueva.
- Qué se registra — la URL de callback, el tipo de evento, el número de intento, el estado, el código HTTP de respuesta, una vista previa de la respuesta, el error (si lo hubo) y las marcas de tiempo del próximo reintento y de la entrega.
- Retención — los registros de entrega se conservan durante 30 días.
- last_used_at — se actualiza al resolver el token con exito; si la actualizacion falla, no es fatal.
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.