Webhooks
Docsbook can notify your systems about events that happen inside a workspace — new content indexed, translations needed, chat questions asked, traffic anomalies and more. Each webhook is typed: you subscribe to one specific event, and Docsbook only POSTs to your URL when that exact event fires.
How it works#
- You register a webhook with
event_type,url, and an optionalsecret. - When the event occurs, Docsbook enqueues a delivery (outbox pattern).
- The worker (Vercel cron, every minute) POSTs the JSON body to your URL.
- We retry up to 3 attempts with exponential backoff (1s, 10s, 60s).
Request format#
POST https://your-url.example.com
Content-Type: application/json
User-Agent: Docsbook-Webhooks/1.0
X-Docsbook-Event: content.indexed
X-Docsbook-Signature-256: sha256=<hex hmac of body>
X-Docsbook-Delivery: 12345
X-Docsbook-Attempt: 1
{
"event": "content.indexed",
"workspace_id": 42,
"occurred_at": "2026-05-23T12:34:56.000Z",
"data": { /* event-specific payload */ }
}Verifying signatures#
import crypto from "node:crypto"
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex")
if (expected !== req.headers["x-docsbook-signature-256"]) reject()A 2xx response = delivered. Anything else triggers retry until the attempt budget is exhausted.
Event catalog#
Registering any webhook requires the Business plan (see Webhook count limits below) — the "Min plan" column below is the additional capability an event itself needs on top of that; for the three "advanced" events, Business already satisfies it.
| Event | Min plan | Payload fields |
|---|---|---|
content.indexed |
Business | pages_count, relations_count, indexed_at |
content.outdated (deprecated — no longer fired automatically) |
Business | last_indexed_at, repo_head_sha |
translation.needed |
Business | source_path, language |
translation.completed |
Business | source_path, language, origin |
translation.outdated |
Business | source_path, language, source_hash_changed |
chat.question_asked |
Business | question, answered, chat_id |
chat.no_answer |
Business | question, chat_id |
chat.negative_feedback |
Business | chat_id, question, answer |
search.no_results |
Business | query |
search.popular |
Business | query, count_24h |
traffic.spike (advanced event) |
Business | path, views, baseline |
traffic.drop (advanced event) |
Business | path, views, baseline |
feedback.received |
Business | path, rating, comment |
plan.upgraded |
Business | from, to |
plan.downgraded |
Business | from, to |
usage.limit_approaching |
Business | metric (ai|translation), used, limit |
mcp.tool_called (advanced event) |
Business | tool_name, args |
Webhook count limits#
Each workspace has a maximum number of active webhooks, based on plan:
| Plan | Max webhooks |
|---|---|
| Free | 0 |
| Pro | 0 |
| Business | 25 |
Webhooks are a Business-exclusive capability — Free and Pro/Pro+ workspaces cannot register any webhooks; only Business unlocks them (up to 25 per workspace).
Registering a webhook#
Via REST#
curl -X POST https://docsbook.io/api/webhooks \
-H "Content-Type: application/json" \
-d '{"workspace_id": 42, "event_type": "content.indexed", "url": "https://you.example.com/hook"}'The response includes the secret exactly once — store it.
Optional Authorization header#
Some receivers (for example a Claude Code routine trigger URL) require their own
bearer token on every request, separate from HMAC signature verification. Pass
auth_header when creating the webhook and Docsbook sends it verbatim as the
Authorization header on every delivery:
curl -X POST https://docsbook.io/api/webhooks \
-H "Content-Type: application/json" \
-d '{"workspace_id": 42, "event_type": "content.indexed", "url": "https://you.example.com/hook", "auth_header": "Bearer sk-..."}'If the value has no space, it's sent as Bearer <value>; if it already contains
a scheme (e.g. Bearer sk-...), it's sent unchanged.
Via MCP#
Each event has a dedicated MCP tool, so an AI agent can subscribe to a specific notification stream without picking strings:
register_webhook_content_indexed(workspace_id: 42, url: "https://...")
register_webhook_translation_needed(repo: "owner/repo", url: "https://...")
register_webhook_traffic_spike(workspace_id: 42, url: "https://...")
Other MCP tools:
list_webhooks(workspace_id)— Freeunregister_webhook(webhook_id)— Freetest_webhook(webhook_id)— Free (enqueues a synthetic ping)list_webhook_deliveries(webhook_id)— Proreplay_webhook_delivery(delivery_id)— Pro
REST endpoints#
GET /api/webhooks?workspace_id=X— listPOST /api/webhooks— createDELETE /api/webhooks/:id— deletePOST /api/webhooks/:id/test— test pingGET /api/webhooks/:id/deliveries— recent deliveriesPOST /api/webhook-deliveries/:id/replay— re-enqueue an existing delivery
Retry & failure semantics#
- Worker runs every minute via Vercel cron.
- A delivery is attempted up to 3 times.
- Backoff is enforced from
created_atof the row: 1s, 10s, 60s. - After the 3rd failure →
status = "failed". Usereplay_webhook_deliveryto re-attempt. - Response code and (truncated) body are stored on every delivery row.