React to Plan changes with webhooks
Receive Dailybot Plan events on your own endpoint: the 25 tasks.* events, the payload envelope without task titles, verifying X-BEARER, and fetching details with your own credential.
Beta
Plan is in beta. Everything under /plan in the web app, the CLI and agent skill commands for projects, goals, boards and tasks, and the /v1/plan/ public API may change before general availability. Want to try it with your team? Write to [email protected].
Polling is right for a screen someone is looking at. For a server that must react to every change (sync to another tool, notify a channel, update a report), subscribe to Plan events and let Dailybot call you.
1. Subscribe through your organization's webhooks
Plan events are delivered through the same outgoing webhooks as every other Dailybot event. There is no separate webhook endpoint under /v1/plan/. Create the subscription in the web app or with the Webhooks API, choose the tasks.* events you need, and set a secret for the X-BEARER header. See Webhooks & events for the setup.
2. The 25 Plan events
| Object | Events |
|---|---|
| Task | tasks.task.created · tasks.task.updated · tasks.task.state_changed · tasks.task.owner_changed · tasks.task.moved · tasks.task.comment_created · tasks.task.archived · tasks.task.participant_added · tasks.task.participant_removed |
| Board | tasks.board.created · tasks.board.updated · tasks.board.archived · tasks.board.restored · tasks.board.member_added · tasks.board.member_removed |
| Project | tasks.project.created · tasks.project.updated · tasks.project.member_added · tasks.project.member_removed · tasks.project.archived · tasks.project.restored |
| Goal | tasks.goal.created · tasks.goal.updated · tasks.goal.archived · tasks.goal.restored |
- There is no
tasks.task.deleted: archive is the delete, so listen fortasks.task.archived. - New events are added over time. Ignore events you do not recognise instead of failing.
- Some changes appear only in the activity feed, not as webhooks (for example a retired milestone, recorded as
project.milestone_deleted).
3. What arrives: identifiers, never titles
Every delivery is a JSON POST with the standard envelope. For Plan, body is the event record:
{
"event": "tasks.task.state_changed",
"event_timestamp": "2026-09-25T10:14:02Z",
"hook": { "id": "wh-1234-abcd", "name": "Tasks sync" },
"body": {
"event": "tasks.task.state_changed",
"occurred_at": "2026-09-25T10:14:02.113954Z",
"observed_at": "2026-09-25T10:14:02.113954Z",
"organization_uuid": "00000000-0000-4000-8000-000000000101",
"actor": { "kind": "user", "uuid": "00000000-0000-4000-8000-00000000000c" },
"correlation_id": "00000000-0000-4000-8000-000000000102",
"entity": { "type": "task", "uuid": "00000000-0000-4000-8000-000000000005", "key": "ENG-142" },
"data": {
"from_state_uuid": "00000000-0000-4000-8000-000000000003",
"to_state_uuid": "00000000-0000-4000-8000-000000000004"
}
}
}
No title, description, comment text or label name is ever in a payload. That is deliberate: a webhook URL is the least trusted place an event can go. The task key is the only human-readable field. When you need the content, fetch it with a credential of your own, which is refused if that credential may not see it:
curl -sS "https://api.dailybot.com/v1/plan/tasks/ENG-142/" \
-H "X-API-KEY: $DAILYBOT_API_KEY" | jq '{key, title, state: .state.name}'
4. Verify every delivery
Deliveries are not signed. Instead, each request carries an X-BEARER header with the secret you set on the subscription. Reject anything else, compare in constant time, and only accept HTTPS:
import { timingSafeEqual } from 'node:crypto';
function isFromDailybot(req) {
const received = Buffer.from(req.headers['x-bearer'] ?? '');
const expected = Buffer.from(process.env.DAILYBOT_WEBHOOK_SECRET);
return received.length === expected.length && timingSafeEqual(received, expected);
}
OAuth 2.0 is also available for webhook authentication; see Webhooks & events.
5. Handle it well
- Answer quickly with a
2xxand do the work asynchronously. - Make processing idempotent: key it on the event record (
entity.uuid,event,occurred_at) so a repeated delivery or your own retry does no harm. - Order by
occurred_at, not by arrival time. - Re-read before acting when the decision depends on the current state; the event says what changed, the API says what is true now.
Reference
- Webhooks & events: subscriptions, envelope and authentication
GET /v1/plan/tasks/{task_id}/GET /v1/plan/tasks/{task_id}/events/: a task’s event log