Webhooks
Frozen surface (dashboard UI)
The dedicated webhooks page in the cloud dashboard is currently frozen by an explicit product decision (2026-07-04, see docs/FROZEN_SURFACES.md in the repository): 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
/webhooksdashboard 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:
{
"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
secretshown 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):
| Event | Trigger |
|---|---|
daemon:connect | Daemon connected to the server |
daemon:disconnect | Daemon connection lost |
agent:status | Agent status changed (throttled, max 1 per 10s per daemon) |
agent:approve_request | Reserved — allow-listed but not currently dispatched by the server |
Dispatched to the server but only delivered to webhooks subscribed with "*" (not individually subscribable):
| Event | Trigger |
|---|---|
agent:generating_started | Agent started generating a response |
agent:generating_completed | Agent finished generating |
agent:waiting_approval | Agent is waiting for approval |
agent:waiting_choice | Agent is waiting on a multiple-choice prompt |
agent:stopped | Agent session stopped |
webhook:test | Explicit test delivery via POST /webhooks/{id}/test — bypasses subscription filtering entirely, delivered only to the one webhook being tested |
Payload Format
{
"event": "agent:generating_completed",
"payload": {
"chatTitle": "Claude Code · myproject",
"ideType": "claude-code",
"duration": 42,
"timestamp": 1714000000000
},
"timestamp": 1714000000000
}Signature Verification
Each webhook request includes:
X-ADHDev-SignatureX-ADHDev-Event
The signature format is:
X-ADHDev-Signature: t=1714000000000,v1=a3f9c2b1...Verify it against the raw request body.
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 '', 200Your webhook secret is shown once when you create the webhook — store it securely.
Management Endpoints
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}/testGET 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
| Plan | Max Webhooks |
|---|---|
| Free | 2 |
| Pro | 10 |
| Ultra | 50 |
| Enterprise | Unlimited |
