Webhooks

Subscribe to changes, verify the signature, and handle delivery correctly.

Webhooks push a signed JSON payload to your endpoint when something changes, so you do not have to poll.

Events

EventFires when
task.createdA task is created
task.updatedA task changes
task.deletedA task is deleted
phase.status_changedA phase's status changes, including gate completion
project.createdA project is created
project.updatedA project changes
comment.createdA comment is posted

An endpoint subscribes to one or more. Anything not in this list is rejected when you register.

Registering an endpoint

curl -X POST https://cadence.alen.world/api/v1/webhooks \
  -H "Authorization: Bearer $CADENCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://example.com/hooks/cadence",
        "events": ["task.created", "task.updated", "phase.status_changed"]
      }'

The response includes a generated secret:

{
  "data": {
    "id": "1c9f…",
    "url": "https://example.com/hooks/cadence",
    "events": ["task.created", "task.updated", "phase.status_changed"],
    "isActive": true,
    "secret": "9a2b…",
    "createdAt": "2026-08-19T09:02:11.482Z"
  }
}

Endpoint management in the app UI is not built yet — register and manage endpoints through the API above.

The payload

Every delivery is a POST with this body:

{
  "event": "task.updated",
  "data": { "id": "7c1e…", "title": "Book line trial", "status": "in-progress" },
  "timestamp": "2026-08-19T09:02:11.482Z"
}

And these headers:

HeaderContains
Content-Typeapplication/json
X-Cadence-EventThe event type, so you can route without parsing the body
X-Cadence-SignatureHMAC-SHA256 of the raw body, keyed with your endpoint secret, hex encoded

Verifying the signature

Compute the HMAC over the raw request body, exactly as received. Parsing and re-serialising the JSON first will change the bytes and the signature will not match.

import { createHmac, timingSafeEqual } from 'node:crypto';
 
function verify(rawBody, signature, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(signature ?? '', 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Always compare with a constant-time function. A plain === on the hex strings leaks timing information.

Delivery behaviour

  • Deliveries are fired after the change has been committed, in parallel to every subscribed endpoint.
  • Each request times out after 10 seconds. Acknowledge quickly and do real work asynchronously — a slow endpoint is a failed endpoint.
  • Any 2xx counts as success; anything else, or a timeout, is recorded as a failure.
  • Every attempt is logged with its status code and the first part of your response body, which is where to look when something is not arriving.

Building a good endpoint

  • Verify first. Reject anything whose signature does not match, before you read the payload.
  • Be idempotent. Handle the same event arriving twice without doubling its effect.
  • Return fast. Acknowledge, then queue.
  • Do not trust the body's shape blindly. Payloads carry the changed record; new fields can appear over time.

Pausing and removing

Set isActive to false with PATCH /webhooks/:id to stop deliveries while keeping the endpoint and its secret — useful during maintenance. Delete it when it is genuinely finished with.

Was this page helpful?

Search the docs

Find a page or a section by name.