Skip to content

Webhooks ​

Frozen surface (dashboard UI)

The dedicated webhooks page in the cloud dashboard is currently frozen by an explicit product decision (2026-07-04): the sidebar entry is hidden and /webhooks redirects to /dashboard. The webhook API and delivery pipeline described below remain live and retained, but no new webhook feature work is planned until the freeze is lifted. Manage webhooks via the REST API surface documented here.

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