workspace:read.
Events
Run events are sent whatever your notification settings, and whether or not you follow the task. Each retry is a separate run, with its own run ID and its own
agent_run.finished.
A subscription belongs to the app connection that created it. Other connections, even the same user’s, can’t see or change it, and disconnecting the app deletes its subscriptions. Settings → API & MCP shows which events each connected app subscribes to.
Create a signing secret
Generate a secret and keep it where your server can read it. It’swhsec_ followed by 24 to 64 random bytes in standard base64. 32 bytes is a good choice:
Subscribe with the API
Your server must be on a public HTTPS URL. Whirl checks the URL before it saves the subscription.workspaceId to get the event from every workspace the connection can access.
The response has the subscription’s id and refreshBefore. Subscriptions last 24 hours, or less if you ask with ttlMs (at least one minute). To keep one, send the same name, url and workspaceId again before refreshBefore. Each connection can have up to 25 subscriptions.
To rotate your secret, subscribe again with a new one. Whirl checks your URL with the new secret, and signs with the old one too for an hour so you can switch over.
list_webhooks shows this connection’s subscriptions, including ones made with MCP Events, and when each last delivered or started failing. It never returns secrets. delete_webhook stops a subscription, and calling it again does nothing. See the API reference for all the fields. These operations are also MCP tools.
Subscribe from an MCP client
MCP clients on protocol version2026-07-28 can use the draft MCP Events extension instead. server/discover advertises it, and events/list, events/subscribe and events/unsubscribe work as the extension describes. Whatever is subscribing, including another MCP server, passes its own URL and secret in delivery:
refreshBefore, and send a new secret to rotate it. To unsubscribe, call events/unsubscribe with the same name, arguments and delivery.url. Clients on older protocol versions can use the webhook tools instead.
Whirl only delivers events to webhooks. There’s no event stream over SSE, and missed events can’t be replayed. cursor is always null, and if you send a cursor to resume from, the response has truncated: true.
Verify your URL
Before it saves a new subscription, Whirl sends your URL a signed POST request:{"challenge": "<random string>"}) or as plain text. Whirl waits up to five seconds for an answer, doesn’t follow redirects, and won’t call private network addresses.
If the check fails, create_webhook returns HTTP status 422 and events/subscribe returns an error. Once a URL passes, Whirl doesn’t check it again for 24 hours for the same connection and secret.
Receive events
Each event is a POST request with a JSON body:organizationId, workspaceId, taskId and title, and a url that opens the task in Whirl. Some events add more:
summary is filled in for approval.required, task.assigned and task.mentioned, and empty for the other events. Events only carry these fields, so read the rest with get_task, get_run or get_pending_action. list_event_types returns each event’s full payload schema.
Check the signature
Requests are signed with Standard Webhooks, so you can use any Standard Webhooks library. They have these headers:
To check a request yourself, compute an HMAC-SHA256 of
<webhook-id>.<webhook-timestamp>.<body>, keyed with the bytes your secret encodes after whsec_. Base64-encode it and compare it with each v1 signature in constant time. Use the raw request body, before you parse it, and reject old timestamps. For example, in Node.js:
Respond and handle retries
Respond with a 2xx status straight away, then do the work. Whirl waits up to five seconds. Events can arrive more than once and out of order, so keep track of the subscription ID andeventId pairs you’ve handled.
Failed events can’t be sent again.
list_webhooks shows when each subscription last delivered (lastDeliveryAt), and when and why it started failing (failedSince and lastError).
When events stop
Whirl checks access again before every attempt, so it doesn’t send an event if:- The subscription expired or was deleted, or the app was disconnected.
- The user lost access to the workspace, or the workspace was archived.
- The task was deleted or moved to another workspace.

