Skip to content

Webhooks ​

Cloud Only

Webhooks are available in the Cloud version only.

Webhooks let ADHDev Cloud push machine and agent events to your own HTTP endpoint.

Important current behavior:

  • the webhook API is live
  • the dedicated /webhooks dashboard page is not currently a stable standard navigation surface
  • use the REST API reference as the canonical contract for payloads, events, and delivery-history shapes
  • webhook create / toggle / delete / test actions are recent-login protected cloud account operations and require a fresh dashboard session JWT, not an API key

Creating a Webhook ​

Create the webhook from a currently signed-in cloud dashboard session. The underlying request body is:

json
{
  "url": "https://example.com/hooks/adhdev",
  "events": ["agent:status", "agent:approve_request"]
}

If you call the mutating webhook endpoints from a stale browser session or with an API key, the server returns 403 RECENT_LOGIN_REQUIRED instead of provisioning the hook.

The create response returns:

  • the webhook record
  • a secret shown once for signature verification

events is an allow-list, not a free-form subscription: only the names below (plus "*") are accepted — anything else is silently dropped. Use ["*"] to subscribe to every event the server dispatches, including the finer-grained ones that cannot be subscribed to individually.

Events ​

Individually subscribable (accepted in events):

EventTrigger
daemon:connectDaemon connected to the server
daemon:disconnectDaemon connection lost
agent:statusAgent status changed (throttled, max 1 per 10s per daemon)
agent:approve_requestReserved — allow-listed but not currently dispatched by the server

Dispatched to the server but only delivered to webhooks subscribed with "*" (not individually subscribable):

EventTrigger
agent:generating_startedAgent started generating a response
agent:generating_completedAgent finished generating
agent:waiting_approvalAgent is waiting for approval
agent:waiting_choiceAgent is waiting on a multiple-choice prompt
agent:stoppedAgent session stopped
monitor:no_progressAgent appears stalled with no progress (legacy alias: monitor:long_generating)
webhook:testExplicit test delivery via POST /webhooks/{id}/test — bypasses subscription filtering entirely, delivered only to the one webhook being tested

Payload Format ​

Every delivery is a JSON body with three top-level fields: event, payload, and timestamp (when the server sent the delivery, epoch milliseconds). For the agent and monitor events (agent:* except agent:status, and monitor:no_progress) the payload looks like this:

json
{
  "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
}
FieldWhen present
daemonIdAlways — the daemon connection that reported the event
event, timestampAlways — the event name and when the daemon observed it (epoch milliseconds)
targetSessionIdWhen the event concerns one agent session — always for agent:generating_completed and agent:stopped
providerTypeWhen the daemon can resolve the session. The agent provider, for example claude-cli or cursor. Absent otherwise — it never carries the daemon kind
daemonTypeAlways. The kind of daemon that sent the event, currently adhdev-daemon
providerSessionIdWhen the provider exposes its own conversation id
workspaceNameWhen the session has a workspace — the working directory of a CLI agent, or the IDE workspace
durationagent:generating_completed only, in whole seconds, when the daemon saw the turn start
elapsedSecmonitor:no_progress — seconds without visible progress
modalMessage, modalButtonsagent:waiting_approval / agent:waiting_choice — the approval prompt text and its button labels
surfaceHidden, mutedWhen the daemon hosts the session — whether it is hidden or muted on the dashboard

daemon:connect, daemon:disconnect, agent:status, and webhook:test carry their own, different payloads. Apart from the approval prompt text in modalMessage, the payload never includes chat messages, chat titles, or agent responses.

Signature Verification ​

Each webhook request includes:

  • X-ADHDev-Signature
  • X-ADHDev-Event

The signature format is:

text
X-ADHDev-Signature: t=1714000000000,v1=a3f9c2b1...

Verify it against the raw request body.

js
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)
})
python
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 '', 200

Your webhook secret is shown once when you create the webhook — store it securely.

Management Endpoints ​

text
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}/test

GET endpoints are the safest automation surface. The mutating endpoints (POST, PATCH, DELETE, test) are recent-login protected dashboard-session operations.

Retry Policy ​

Failed deliveries are retried up to 3 times with exponential backoff.

Plan Limits ​

PlanMax Webhooks
Free2
Pro10
Ultra50
EnterpriseUnlimited

Hosted cloud docs live here. Open-source and self-hosted docs live in the OSS repository.