Webhooks
Subscribe to events at the org level. Each delivery is signed with the webhook's secret using HMAC-SHA256; verify the signature before trusting the payload.
Generated from src/lib/api-paths/webhooks.ts. The events list comes from CreateWebhookSchema in the runtime, what's documented here is what the API accepts.
get/api/orgs/{slug}/webhooks
List org webhooks
Every webhook configured for the org. Secrets are returned as a preview only.
Auth: Bearer token (API key or OAuth access token).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | yes |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | Webhook list. |
post/api/orgs/{slug}/webhooks
Create a webhook
Register a webhook subscription. The full signing secret is returned exactly once in the response — store it before the response is closed.
Auth: Bearer token (API key or OAuth access token).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
url | string (uri) | yes | |
events | "row.created" | "row.updated" | "row.deleted" | "row.sealed" | "comment.added" | "comment.deleted" | "member.invited" | "member.joined" | "member.removed" | "member.role_changed" | "workspace.created" | "workspace.renamed" | "workspace.columns_updated" | "workspace.visibility_changed" | "workspace.archived" | "doc.created" | "doc.updated" | "doc.heading_added" | "doc.mention_added" | "message.sent" | "file.uploaded" | "file.renamed" | "file.moved" | "file.archived" | "file.restored" | "file.version_replaced" | "file.starred" | "file.tagged" | "file.described" | "file.shared" | "file.share_revoked" | "file.commented" | "folder.created" | "folder.archived"[] | no |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | Created. |
400 | Error | URL failed SSRF guard (loopback, private range, cloud metadata). |
patch/api/orgs/{slug}/webhooks/{id}
Toggle webhook active state
Enable or disable a webhook without deleting it. Body: `{ active: boolean }`.
Auth: Bearer token (API key or OAuth access token).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | yes | |
id | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | yes |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | Updated. |
404 | Error | Not found. |
delete/api/orgs/{slug}/webhooks/{id}
Delete a webhook
Removes the subscription. In-flight deliveries are cancelled.
Auth: Bearer token (API key or OAuth access token).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | yes | |
id | string | yes |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | Deleted. |
404 | Error | Not found. |
get/api/orgs/{slug}/webhooks/{id}/deliveries
List recent webhook deliveries
Up to 50 most recent delivery attempts for one webhook, newest first. Use to debug failed integrations or watch retry status.
Auth: Bearer token (API key or OAuth access token).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | yes | |
id | string | yes |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | Delivery list. |
404 | Error | Webhook not found. |
post/api/orgs/{slug}/webhooks/{id}/rotate-secret
Rotate webhook signing secret
Generate a fresh signing secret. Returned exactly once. Subsequent deliveries are signed with the new secret only — update your receiver before the next event lands.
Auth: Bearer token (API key or OAuth access token).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | yes | |
id | string | yes |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | Rotated. |
404 | Error | Not found. |
Events
Pass any subset on events at create time. Adding new event names later requires a schema update in src/lib/schemas.ts + Settings UI; the runtime rejects unknown names.
| Event | When it fires |
|---|---|
row.created | A new row is appended. |
row.updated | Any cell on a row is changed. Includes the diff. |
row.deleted | A row is deleted. |
row.sealed | A row is sealed (writes locked). |
comment.added | A comment is created on any anchor type. |
comment.deleted | A comment is deleted. |
member.invited | An invite is created (workspace or org). |
member.joined | An invitee accepts and a membership row is created. |
member.removed | A member is removed from a workspace, including the cascade from an org-removal. |
member.role_changed | A member's role changes (workspace or org). |
workspace.created | A workspace is created. |
workspace.renamed | A workspace's name (or slug) changes. |
workspace.columns_updated | Columns are added, removed, reordered, or retyped on a table surface. |
workspace.visibility_changed | Workspace visibility flips between private / org / unlisted / public. |
workspace.archived | A workspace is archived (soft-delete). |
doc.created | A workspace is created in doc mode. |
doc.updated | A doc body is replaced or appended-to. |
doc.heading_added | A new heading is written in a doc body. Useful for outline-aware downstream pipelines. |
doc.mention_added | A user or agent is @-mentioned in a doc body. Payload carries `mentions: [{ kind: "user" | "agent", id, label }]` for newly-added mentions only, re-saving a doc that already mentions someone does not re-fire. Mentioned humans also get an inbox row + email; mentioned agents get this webhook. |
file.uploaded | A new file row is committed to a Files surface (via direct or proxy upload). Payload: `{ fileId, name, mimeType, size, surfaceId, parentFolderId }`. |
file.renamed | PATCH /files/{id} { name } changed the display name. blobKey stays stable. Payload: `{ fileId, name, previousName, surfaceId }`. |
file.moved | PATCH /files/{id} { parentFolderId } moved a file within its surface. Cross-surface moves reject. Payload: `{ fileId, parentFolderId, previousParentFolderId, surfaceId }`. |
file.archived | DELETE /files/{id} soft-deleted a file (30-day trash window). Payload: `{ fileId, name, surfaceId }`. |
file.restored | PATCH /files/{id} { restore: true } un-archived a soft-deleted file. Payload: `{ fileId, name, surfaceId }`. |
file.version_replaced | POST /files/{id}/versions replaced bytes (same id, new sha256). Payload: `{ fileId, name, previousSha256, sha256, size, surfaceId }`. |
file.starred | PATCH /files/{id}/metadata { starred } flipped the workspace-scoped star state. Payload: `{ fileId, name, starred, surfaceId }`. |
file.tagged | PATCH /files/{id}/metadata { tags } updated the tag set. Payload: `{ fileId, name, tags, surfaceId }`. |
file.described | PATCH /files/{id}/metadata { description } updated the free-text caption. Payload: `{ fileId, name, description, surfaceId }`. |
file.shared | POST /files/{id}/share minted a public share token. Payload: `{ fileId, name, shareTokenId, url, surfaceId }`. |
file.share_revoked | DELETE /files/{id}/share revoked a previously-minted token. Payload: `{ fileId, shareTokenId, surfaceId }`. |
file.commented | A comment was added with `target.type="file"`. Fires alongside the generic `comment.added` so file-focused subscribers don't have to filter the firehose. Payload: `{ fileId, name, surfaceId, commentId, parentId, body, mentions }`. |
folder.created | POST /folders created a folder. Payload: `{ folderId, name, parentFolderId, surfaceId }`. |
folder.archived | DELETE /folders/{id} cascade-archived a folder and its descendants. Payload: `{ folderId, name, surfaceId, cascaded }` (cascaded = count of files moved to trash). |
Signing
Every delivery includes a Dock-Signature header of the form t=<ts>,v1=<hex>. Compute HMAC_SHA256(secret, ts + "." + body); constant-time-compare against v1; reject if ts is more than 5 minutes off your clock.
For 24h after a secret rotation, deliveries carry a second signature v2=<hex> signed with the previous secret. Verify against v1 first; if it fails, fall back to v2. After 24h, only v1 is sent. This gives you a safe window to swap your stored secret without dropping events in flight.
Frequently asked questions
- How do I register a Dock webhook to react to row changes?
- POST `/api/webhooks` with `{ url, events: ['row.created', 'row.updated'] }`. URL must be public HTTPS (loopback, RFC1918 private ranges, cloud metadata addresses are blocked). Returns the signing secret exactly once.
- What events does Dock emit via webhooks?
- row.* (created/updated/deleted/sealed/moved_surface), doc.* (created/updated/heading_added/mention_added), comment.* (added/deleted), file.* (uploaded/renamed/moved/archived/restored/version_replaced/starred/tagged/described/shared/share_revoked/commented), folder.* (created/archived), member.* (invited/joined/removed/role_changed), workspace.* (created/renamed/columns_updated/visibility_changed/archived/unarchived), surface.* (created/updated/archived). For agent-routing on @-mentions: subscribe to `doc.mention_added` for doc-body pings and read the `mentions` array on `comment.added` for comment pings.
- How do I verify a Dock webhook signature?
- Read the `Dock-Signature` header (format `t=<unix-ts>,v1=<hex>`). HMAC-SHA256 the body with your signing secret prefixed by `<ts>.`; constant-time-compare against `v1`. Reject if `t` is more than 5 minutes off your clock (replay protection).
- How do I rotate a Dock webhook signing secret?
- POST `/api/webhooks/:id/rotate-secret`. New secret returned exactly once. For 24h after rotation, every delivery carries BOTH the new signature (`v1=…`) and the previous one (`v2=…`) so receivers have a safe window to swap their stored secret without dropping in-flight events. After 24h, only `v1` is sent.
- How do I temporarily pause a Dock webhook?
- PATCH `/api/webhooks/:id` with `{ active: false }`. Inactive webhooks skip delivery (no retry queue, no log row), but the URL + secret + event subscription are preserved. Flip back with `{ active: true }`.
- How do I delete a Dock webhook permanently?
- DELETE `/api/webhooks/:id`. URL stops receiving events immediately and the secret is destroyed; recreate from scratch if needed. To pause without losing config, use `PATCH … active:false` instead.
- What happens if my Dock webhook endpoint is down?
- Dock retries with exponential backoff (1m, 5m, 30m, 2h, 12h, 24h) for up to 24 hours. After max retries, the delivery is marked failed in the webhook log. Re-send manually via the dashboard or POST `/api/webhooks/:id/deliveries/:deliveryId/redeliver`.
- Can I filter which Dock events fire which webhook?
- Per-webhook event subscription via the `events` array on POST/PATCH. Within an event kind, no built-in body-level filter; do per-event filtering in your handler (the payload includes `principal`, `workspace`, the changed object).
- What's the rate limit on Dock webhook deliveries?
- Plan-tier-scoped: Free 1k/mo, Pro 10k/mo, Scale 100k/mo. Per-org budget across all webhooks. Hitting the cap returns 402 on subsequent event triggers; existing deliveries continue but new ones queue.
- How do I send Dock events to Slack?
- Register a Slack incoming webhook URL (workflow apps work too) as the Dock webhook endpoint. Map Dock's payload shape into Slack's expected shape with a thin proxy (Cloudflare Worker, AWS Lambda) or use n8n / Zapier as the bridge for no-code routing.
Related
- Comments API: the source of every
comment.*event listed above. - Agent attribution: every event payload carries the principal that caused it.
- Rate limits: webhook send caps per plan.
- Error codes