Skip to main content
Outbound webhooks push page activity to external systems — CI, data warehouses, Slack bridges, custom apps — instead of making them poll.

Events

Personal-space activity never emits webhooks. A subscription can cover all shared spaces or be filtered to one space.

Delivery contract

Deliveries are POSTs with a JSON body:
Headers: X-Coco-Event, X-Coco-Delivery, X-Coco-Webhook, and X-Coco-Signature — an HMAC-SHA256 of the raw body with the webhook’s signing secret, formatted sha256=<hex>.

Verifying a signature

Non-2xx responses and network failures retry with backoff (3 retries by default). Every attempt’s outcome lands in the delivery log (GET /admin/webhooks/:id/deliveries, or the Deliveries panel in the admin UI).

Management (org admin)

  • GET/POST /admin/webhooks — list / create ({url, events, spaceSlug?, description?}; the signing secret is returned once on create)
  • PATCH /admin/webhooks/:id — update URL, events, space filter, enable/disable
  • POST /admin/webhooks/:id/rotate-secret — new secret, returned once
  • POST /admin/webhooks/:id/test — send a webhook.test delivery and report the outcome
  • DELETE /admin/webhooks/:id

Security posture

Webhook signing secrets are always stored encrypted. Webhooks require COCO_SETTINGS_ENCRYPTION_KEY on the API runtime (create/rotate return a clear 503 without it).
  • Endpoints must be https; plain http requires the deployment to opt in with COCO_WEBHOOKS_ALLOW_HTTP=1 (payloads then transit unencrypted).
  • SSRF guard: private, loopback, link-local, CGNAT, and reserved addresses (IPv4 and IPv6, including v4-mapped forms) are rejected — literals at create time, and again at delivery time after DNS resolution, so a hostname pointing at an internal address is blocked too (recorded in the delivery log, no retries). A small time-of-check/time-of-use window inherent to DNS remains; deployments with strict requirements should add egress filtering. COCO_WEBHOOKS_ALLOW_PRIVATE_URLS=1 disables the guard for intentionally internal deployments.