For Builders

Webhooks

A webhook is the door your code uses to enter Daguito. Any external system —your backend, a CRM, a cron job— can trigger a flow with a single call. There are two transports: HTTP for one-shot calls and WebSocket for long-running streaming flows. Every token is stored hashed, every outbound delivery is signed, and everything is recorded.

What a webhook is

A webhook is a named entry point that points at a flow. You hand a token to an external system and that system triggers the flow whenever it needs to. You do not expose your API: you expose a narrow, signed endpoint that only knows how to run that one flow.

You pick the transport when you create the webhook, based on how you will consume it:

Info

Tokens are never stored in plaintext. A SHA-256 hash is stored to resolve them in O(1), and the plaintext is returned only once, when you create the webhook.

Types and transport

The webhook's type field decides which endpoint its token activates. There are two values: http and streaming.

HTTP webhooks

An HTTP webhook exposes two modes, depending on how long the caller can wait:

Streaming webhooks

A streaming webhook opens a bidirectional WebSocket at /v1/webhooks/:id/stream. The token travels as a ?token= query param on the connection. The client drives a session with frames and receives the flow's events live.

These are the three ways to invoke a webhook, depending on its type:

# 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_...
Warning

The type is immutable: it is fixed at creation and never changes. To move from HTTP to streaming you must create a new webhook.

Tokens and security

Each webhook has a token it authenticates with. The model is simple and strict:

// 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
Warning

There is no automatic token rotation and no delete endpoint. To invalidate a token, set the webhook's status to revoked; for a new one, create another webhook.

Synchronous HTTP: POST /h/:token

This is the most direct path: a single call that blocks until the flow finishes. Ideal for integrations that need the response immediately.

From the SDK, use runWebhook for this synchronous call:

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 }

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

When you don't want to block, this mode enqueues the message and responds instantly. The flow runs separately and results arrive via callbacks.

Warning

This route isn't meant for very long flows: it processes the delivery right away. For long-running processes, it's best to split them into shorter steps.

Outbound callbacks

A callback is a server-to-server POST that Daguito sends to a URL you provide when a flow event occurs. That way you receive results without polling.

Payload shape

// 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 */ }
}

Verify the signature

Recompute the HMAC over the raw body (without re-serializing) with your secret and compare it to the header. If it doesn't match, reject the request.

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

Every callback attempt, successful or failed, is logged so you can debug your deliveries.

Streaming over WebSocket

For long flows —LLM generations, vision pipelines, several tools chained— the WebSocket avoids the proxy limit and delivers events as they happen.

Session lifecycle

The client drives a session with frames: session.start creates the session_key, session.open triggers the flow with the base_input, message sends text or audio, and session.end closes it. The server confirms each step and pushes FlowEvent frames.

// 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

Events reaching the client are filtered by the webhook's events_downstream array: wildcard patterns (for example flow.* or node.token) compiled once per session.

The SDK wraps the whole session in runWebhookStream, which has no 100s proxy limit:

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 }
Warning

Reconnection is not automatic. Each socket authenticates separately with the token, so your client must reconnect and reopen the session if the connection drops.

Flow events

Both callbacks and the WebSocket deliver FlowEvent: the same event model that travels through a flow's execution from start to finish.

Event types

The event envelope

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
}

Wildcard filters

Both events (callbacks) and events_downstream (streaming) accept wildcard patterns. The dot separates segments and the asterisk acts as a wildcard:

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

Configuration and limits

Webhook configuration lives in config and depends on the type. These are the main limits:

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
}
Warning

On streaming webhooks, events_downstream is required and has no default. If you omit it, the allowlist is empty and the client will receive no events.

Auditing and observability

Every outbound delivery is recorded so you can debug and retry. Daguito keeps a delivery log you can inspect.

Info

Each failed delivery is logged with its status and the time of the next retry. Automatic retry is on our roadmap.

What's next?