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:
- HTTP — for request/response integrations. You call, you wait, you get the output.
- Streaming — a bidirectional WebSocket channel for long flows and live events.
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:
- Synchronous —
POST /h/:token. The token is the only authentication. It blocks until the flow finishes and returns the output in the response. - Asynchronous —
POST /v1/webhooks/:id/messageswith Bearer auth. It returns 202 instantly and the flow runs in the background; results arrive via callbacks.
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_... 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:
- Format — a
dgsk_wh_prefix followed by 24 random bytes, base64url-encoded. The legacysk_wh_prefix is also accepted. - Storage — a SHA-256 hash is stored, never the plaintext token. The plaintext does not persist after creation.
- Preview — the dashboard shows
dgsk_wh_...LAST4, with the last 4 characters, enough to recognize it without exposing it. - Shown once — the plaintext is returned in the create response and cannot be retrieved later. Store it in your secrets manager.
// 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 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.
- Request — the body is free-form JSON and is handed to the flow as
base_input. - Response —
{ ok, execution_id, status, output }. - Timeout — the edge proxy cuts off at 100 seconds. For longer flows use the streaming transport.
- Billing — the route checks the organization's billing posture and returns 402 if it is not in good standing.
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.
- Request —
{ session_key, message, base_input? }, where message is the normalized message object. - Response — an instant 202 with
{ ok, status, message_id, session_key }. - Auth — Bearer token read from the
Authorizationheader. - When to use it — high-volume integrations that need low latency and cannot wait for the result.
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.
- Filter — each callback declares an
eventsarray of patterns (for exampleflow.completedornode.*). Only those events fire the POST. - Signature — the body is signed with HMAC-SHA256 using
secret_hash, in thex-daguito-signature: sha256=<hex>header. - Headers — it also carries
x-daguito-event(type) andx-daguito-webhook-id(webhook id). - Payload —
{ event, webhook_id, session_key, flow_id, at, data }, where data depends on the event.
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')
} 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 } 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
- Flow lifecycle —
flow.started,flow.resumed,flow.completed,flow.failed,flow.paused,flow.branch. - Nodes —
node.started,node.completed,node.progress,node.emit,node.token. - Sync and control —
merge.progressandhandoff.requested.
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:
- http.timeout_ms — between 1 and 60000 ms, defaulting to 30000.
- streaming.events_downstream — required on streaming webhooks; defines which events reach the client.
- audio —
max_session_seconds,max_bytes_per_sessionandmax_concurrent_per_webhook(0 = no cap). - name — between 1 and 200 characters.
- Callback URL — a valid URL, max 2048 characters; the events array needs at least one entry.
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
} 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.
- Append-only — the table is never updated; each retry inserts a new row.
- What's logged — the callback URL, event type, attempt number, status, HTTP response code, a response preview, the error (if any), and the next-retry and delivered timestamps.
- Retention — delivery logs are kept for 30 days.
- last_used_at — updated whenever the token resolves successfully; if that update fails, it is non-fatal.
Each failed delivery is logged with its status and the time of the next retry. Automatic retry is on our roadmap.