Alerts and webhooks
Set up an alert#
Alerts work on every plan, and deliveries are never metered.
Open Integrations, search the catalog and open the destination's card:
- Slack alerts, Discord alerts or Microsoft Teams alerts — a message in a channel
- PagerDuty — an incident on a service
- Email alerts — a message in an inbox
- Claude — wakes a Claude Code routine
- Custom webhook — the signed JSON envelope, sent to your own endpoint
Paste the channel's incoming-webhook URL, the PagerDuty integration key, the routine's trigger URL or your endpoint. An email address gets a confirmation link first and receives nothing until it is clicked.
Choose the event under Send when — the list offers only events that fire — and click Connect. One endpoint carries one event; Add endpoint adds the next.
Fill in Signing secret (optional) with 16 characters or more and keep a copy: it is what you verify deliveries with, and a secret the panel generates for you is never shown. Authorization header goes out on every delivery, a bare token as Bearer <token>; Claude routines need one.
Set it up from your agent#
Each event has its own register_webhook_<event> tool, kept off your client's tool list: find it with find_tool, then run it with call_tool.
{
"name": "register_webhook_chat_no_answer",
"arguments": {
"workspace_id": "acme/docs",
"url": "https://hooks.example.com/docsbook",
"secret": "at-least-16-characters-you-keep"
}
}The reply includes the webhook's secret, generated when you pass none. A Slack, Discord or Anthropic URL is recognised by its host and formatted for that app; any other URL gets the signed envelope.
Event catalog#
Sixteen events fire today. Each arrives named in underscore form, as below, in the X-Docsbook-Event header and the envelope's event field; the panel shows the same names with dots, as in chat.no_answer.
| Event | Fires when | data fields |
|---|---|---|
chat_question_asked |
The AI chat answered a question — on your site, over the API or through ask_project_docs |
session_id, question, answer, language, has_answer, latency_ms |
chat_no_answer |
The chat's answer said it didn't know | session_id, question, language |
chat_negative_feedback |
A reader rated a page down; sent alongside feedback_received |
session_id, page_path, type, comment |
feedback_received |
A reader rated a page up or down | page_path, vote, comment, country |
search_no_results |
A search on your docs found nothing; once per burst of typing | query, language |
content_indexed |
The semantic index finished a run in which pages changed; needs Semantic Search on | pages_added, pages_removed, pages_modified, pages_count, relations_count, commit_sha, indexed_at |
content_indexing_failed |
An index run stopped for good: the repository was unreadable or embedding failed | reason, granularity, units_failed |
translation_needed |
A pass found pages with no fresh translation; one event per language per commit | source_path, language, source_blob_sha, commit_sha, paths, count |
translation_completed |
A page's translation was saved | source_path, language, origin |
translation_outdated |
A translation was redone because its source page changed | source_path, language, source_hash_old, source_hash_new |
plan_upgraded |
The project moved up a plan | workspace_id, old_plan, new_plan |
plan_downgraded |
The project dropped a plan: cancelled, past due, or its free credit ran out | workspace_id, old_plan, new_plan, reason |
usage_limit_approaching |
Spend crossed a warning step on one of the project's budgets | workspace_id, metric, used, limit, percent |
usage_overage_limit_reached |
The month's overage cap was reached | workspace_id, overage_spent_cents, overage_limit_cents |
project_domain_connected |
A custom domain was attached to the project | domain, connected_at |
project_created |
The project was created; it fires once, so a webhook added later never gets it | template_id, imported, created_at |
Plans appear as free, pro and business, the stored name of Enterprise.
Five more names have a register_webhook_* tool, but nothing sends them today, so a webhook on one stays silent:
content_outdatedsearch_populartraffic_spikeandtraffic_dropmcp_tool_called
What a delivery looks like#
A Custom webhook receives a POST with these headers:
POST /docsbook HTTP/1.1
Content-Type: application/json
User-Agent: Docsbook-Webhook/1.0
X-Docsbook-Event: chat_no_answer
X-Docsbook-Channel: api
X-Docsbook-Signature-256: sha256=<hex digest>The body is the event envelope; data holds the catalog fields plus schema_version, which is 1.
{
"event": "chat_no_answer",
"workspace_id": 42,
"timestamp": "2026-09-24T09:30:12.000Z",
"data": {
"schema_version": 1,
"session_id": "<session id>",
"question": "How do I rotate an API key?",
"language": "en"
}
}- Chat apps and PagerDuty — Slack, Discord, Microsoft Teams and PagerDuty get a message shaped for that app instead.
- Email — the inbox gets a formatted email, unsigned.
- Claude — the routine gets the envelope plus a
textprompt describing the event. - Test pings — carry
"test": trueand a shortmessageindata.
Verify the signature#
X-Docsbook-Signature-256 is sha256= followed by the hex HMAC-SHA256 of the exact request body, keyed with the webhook's signing secret. Compute it over the raw bytes, before parsing the JSON, and compare in constant time.
import crypto from "node:crypto"
export function isFromDocsbook(rawBody, signatureHeader, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(signatureHeader ?? "")
return a.length === b.length && crypto.timingSafeEqual(a, b)
}import hashlib
import hmac
def is_from_docsbook(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")Reject any request whose signature does not match: it did not come from Docsbook, or it was changed on the way.
Retries, tests and replays#
A delivery succeeds when your endpoint answers 2xx within 15 seconds.
- Retried — deliveries go out from a worker that runs every 5 minutes; a failed attempt is retried on the next run, up to 3 attempts in all, and then the delivery is marked
failed. - Recorded — a delivery keeps its last attempt's response code and the first 2,000 characters of the response body.
- Tested —
test_webhooksends a test ping right away, outside the 5-minute cycle. - Replayed — Replay on a delivery in Activity ▸ All events, or
replay_webhook_delivery, sends the same payload again at once. - Listed and removed —
list_webhooksandlist_webhook_deliveriesshow endpoints and attempts; Disconnect on the integration, orunregister_webhook, removes an endpoint.
See every event#
Activity ▸ All events lists everything the project produced, whether or not an alert was watching. Open a row for its Payload and each Delivery with its Replay button; an event nothing forwarded offers Wake an agent on events like this.
An alert tells a person. To have the agent act on the same event, switch on a card under Triggers: Answer what the chat could not wakes on chat_no_answer, and Fix what readers rated down on feedback_received.