Webhooks
Webhooks push events out of Conatus as they happen. Register endpoints in Settings → Webhooks.
Registering an endpoint
Section titled “Registering an endpoint”The URL must use HTTPS, or be on localhost for local development. Anything
else is rejected.
When the endpoint is created you are shown a secret, once. Copy it. Only its stored copy remains, and every payload is signed with it. If you lose it, delete the endpoint and create a new one.
Each endpoint shows its status (Active or Disabled) and its consecutive failure count. A disabled endpoint has a Re-enable button, which also resets it.
Events
Section titled “Events”| Event | Fires when |
|---|---|
task.created |
A task is created |
task.completed |
A task is completed |
task.uncompleted |
A completed task is reopened |
task.deleted |
A task is moved to Trash |
comment.added |
A comment is posted on a task or a project |
project.created |
A project is created |
project.archived |
A project is archived |
project.deleted |
A project is moved to Trash |
These are exactly the events the activity log records. Field edits do not fire webhooks: renaming, re-prioritising, rescheduling, reordering, label changes.
Events are scoped to you: your endpoints receive events from your actions, not from a collaborator working in a project you share.
Payload
Section titled “Payload”Every delivery is a POST with this body:
{ "type": "task.completed", "taskContent": "Renew passport", "projectId": "9c1f…", "projectName": "Admin", "occurredAt": "2026-03-03T09:14:22.104Z"}taskContent and projectName are snapshots taken at the time of the event, so
a task.deleted payload still names what was deleted. For a project-level event,
taskContent carries the project name. projectId may be null.
Verifying the signature
Section titled “Verifying the signature”Each request carries X-Webhook-Signature: the HMAC-SHA256 of the raw request
body, keyed with the endpoint’s secret. Compute it over the bytes you received,
before any JSON parsing or re-serialisation. A re-encoded body will not match.
Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) { const expected = createHmac("sha256", secret).update(rawBody).digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(header ?? ""); return a.length === b.length && timingSafeEqual(a, b);}Python:
import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool: expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header or "")Always compare in constant time, and reject anything that does not match.
Delivery, retries and failure
Section titled “Delivery, retries and failure”- Deliveries go through a durable job queue in PostgreSQL, so a restart mid-flight does not lose them.
- Each attempt times out after 10 seconds.
- Any non-2xx response, or a timeout, counts as a failure and is retried up to 5 times with backoff.
- A successful delivery resets the endpoint’s consecutive failure count to zero.
- After 20 consecutive failures the endpoint is disabled automatically. It stops receiving deliveries until you re-enable it in Settings.
Failures are also reported through ERROR_WEBHOOK_URL if you have configured
one.
Ordering is not guaranteed. Use occurredAt if you need to sequence events, and
make your handler idempotent. A retry can deliver the same event twice.
Testing locally
Section titled “Testing locally”localhost URLs are accepted, so you can point an endpoint at a local listener
while Conatus runs in Compose on the same machine. From inside the container the
host is reachable as host.docker.internal on Docker Desktop; on Linux, add the
host-gateway mapping or use the host’s LAN address.
