Webhooks
Solo en la nube
Los webhooks están disponibles solo en la versión Cloud.
Los webhooks permiten que ADHDev Cloud envíe eventos de máquina y agente a tu propio endpoint HTTP.
Comportamiento actual importante:
- la API de webhooks está activa
- la página dedicada
/webhooksdel panel no es actualmente una superficie de navegación estándar estable - usa la referencia de la REST API como el contrato canónico para las cargas útiles, los eventos y las formas del historial de entrega
- las acciones de crear / alternar / eliminar / probar webhook son operaciones de cuenta de la nube protegidas por inicio de sesión reciente y requieren un JWT de sesión de panel nuevo, no una clave de API
Crear un webhook
Crea el webhook desde una sesión de panel de la nube con la sesión actualmente iniciada. El cuerpo de la solicitud subyacente es:
{
"url": "https://example.com/hooks/adhdev",
"events": ["agent:status", "agent:approve_request"]
}Si llamas a los endpoints de webhook que mutan desde una sesión de navegador obsoleta o con una clave de API, el servidor devuelve 403 RECENT_LOGIN_REQUIRED en lugar de aprovisionar el hook.
La respuesta de creación devuelve:
- el registro del webhook
- un
secretmostrado una vez para la verificación de firma
events es una lista de permitidos, no una lista libre: solo se aceptan los nombres de abajo (más "*") — cualquier otro se descarta silenciosamente. Usa ["*"] para suscribirte a todos los eventos que el servidor despacha, incluidos los más granulares que no se pueden suscribir individualmente.
Eventos
Suscribibles individualmente (aceptados en events):
| Evento | Disparador |
|---|---|
daemon:connect | El daemon se conectó al servidor |
daemon:disconnect | Se perdió la conexión del daemon |
agent:status | Cambió el estado del agente (limitado, máx. 1 cada 10s por daemon) |
agent:approve_request | Reservado — está en la lista de permitidos pero el servidor no lo despacha actualmente |
Despachados por el servidor pero solo entregados a webhooks suscritos con "*" (no suscribibles individualmente):
| Evento | Disparador |
|---|---|
agent:generating_started | El agente empezó a generar una respuesta |
agent:generating_completed | El agente terminó de generar |
agent:waiting_approval | El agente está esperando aprobación |
agent:waiting_choice | El agente está esperando una selección múltiple |
agent:stopped | La sesión del agente se detuvo |
monitor:no_progress | El agente parece estancado sin progreso (alias legado: monitor:long_generating) |
webhook:test | Entrega de prueba explícita vía POST /webhooks/{id}/test — omite el filtrado de suscripción por completo, se entrega solo al webhook que se está probando |
Formato de la carga útil
Cada entrega es un cuerpo JSON con tres campos de nivel superior: event, payload y timestamp (cuándo envió el servidor la entrega, en milisegundos epoch). Para los eventos de agente y de monitor (agent:* salvo agent:status, y monitor:no_progress) la carga útil tiene este aspecto:
{
"event": "agent:generating_completed",
"payload": {
"daemonId": "8e1f4c2a9b7d3e5f6a0c1b2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f",
"daemonType": "adhdev-daemon",
"event": "agent:generating_completed",
"timestamp": 1714000000000,
"targetSessionId": "9b2d7c4e-1f3a-4b5c-8d6e-0a1b2c3d4e5f",
"providerType": "claude-cli",
"providerSessionId": "3f2a9c1e-5b7d-4e8a-9c0f-1a2b3c4d5e6f",
"workspaceName": "/home/dev/projects/myproject",
"duration": 42,
"surfaceHidden": false,
"muted": false
},
"timestamp": 1714000000215
}| Campo | Cuándo aparece |
|---|---|
daemonId | Siempre: la conexión del daemon que informó del evento |
event, timestamp | Siempre: el nombre del evento y cuándo lo observó el daemon (milisegundos epoch) |
targetSessionId | Cuando el evento se refiere a una sesión de agente concreta; siempre en agent:generating_completed y agent:stopped |
providerType | Cuando el daemon puede resolver la sesión. El proveedor del agente, por ejemplo claude-cli o cursor. Si no, el campo no aparece; nunca contiene el tipo de daemon |
daemonType | Siempre. El tipo de daemon que envió el evento, actualmente adhdev-daemon |
providerSessionId | Cuando el proveedor expone su propio id de conversación |
workspaceName | Cuando la sesión tiene un espacio de trabajo: el directorio de trabajo de un agente CLI o el espacio de trabajo del IDE |
duration | Solo en agent:generating_completed, en segundos enteros, cuando el daemon vio empezar el turno |
elapsedSec | monitor:no_progress: segundos sin progreso visible |
modalMessage, modalButtons | agent:waiting_approval / agent:waiting_choice: el texto de la solicitud de aprobación y las etiquetas de sus botones |
surfaceHidden, muted | Cuando el daemon aloja la sesión: si está oculta o silenciada en el panel |
daemon:connect, daemon:disconnect, agent:status y webhook:test envían cargas útiles propias con otra forma. Aparte del texto de la solicitud de aprobación en modalMessage, la carga útil nunca incluye mensajes de chat, títulos de chat ni respuestas del agente.
Verificación de firma
Cada solicitud de webhook incluye:
X-ADHDev-SignatureX-ADHDev-Event
El formato de la firma es:
X-ADHDev-Signature: t=1714000000000,v1=a3f9c2b1...Verifícala contra el cuerpo de la solicitud en bruto.
import crypto from 'crypto'
function verifySignature(body, signature, secret) {
const parts = Object.fromEntries(signature.split(',').map((p) => p.split('=')))
const timestamp = parts.t
const received = parts.v1
const payload = `${timestamp}.${body}`
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(received),
Buffer.from(expected)
)
}
// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-adhdev-signature']
const body = req.body.toString('utf8')
if (!verifySignature(body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature')
}
const event = JSON.parse(body)
// handle event...
res.sendStatus(200)
})import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
parts = dict(p.split('=', 1) for p in signature.split(','))
timestamp = parts.get('t', '')
received = parts.get('v1', '')
payload = f"{timestamp}.{body.decode('utf-8')}".encode()
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(received, expected)
# Flask example
@app.route('/webhook', methods=['POST'])
def webhook():
sig = request.headers.get('X-ADHDev-Signature', '')
if not verify_signature(request.data, sig, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
# handle event...
return '', 200Tu secreto de webhook se muestra una vez cuando creas el webhook — guárdalo de forma segura.
Endpoints de gestión
GET /api/v1/webhooks
POST /api/v1/webhooks
PATCH /api/v1/webhooks/{webhookId}
DELETE /api/v1/webhooks/{webhookId}
GET /api/v1/webhooks/{webhookId}/deliveries?limit=20
POST /api/v1/webhooks/{webhookId}/testLos endpoints GET son la superficie de automatización más segura. Los endpoints que mutan (POST, PATCH, DELETE, test) son operaciones de sesión de panel protegidas por inicio de sesión reciente.
Política de reintentos
Las entregas fallidas se reintentan hasta 3 veces con backoff exponencial.
Límites de plan
| Plan | Máx. webhooks |
|---|---|
| Free | 2 |
| Pro | 10 |
| Ultra | 50 |
| Enterprise | Ilimitado |
