Skip to content

Webhooks

Webhooks let CAVS call you when something happens: a push lands, a release is tagged, a quota is about to bite. Deliveries are signed with HMAC-SHA256.

  1. Go to Organization settings → Webhooks → New webhook.

  2. Enter your endpoint URL (http or https; use https) and pick the events to subscribe to. * subscribes to everything.

  3. Copy the signing secret. CAVS generates it (prefixed whsec_) and returns it exactly once.

Terminal window
curl -sS -X POST "https://cavsnode.com/api/v1/organizations/acme-ai/webhooks" \
-H "Authorization: Bearer $CAVS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/cavs",
"events": ["repository.push", "release.published"]
}'
Response — the secret appears only here
{
"webhook": {
"id": "7a2c…",
"organization_id": "0a11…",
"url": "https://hooks.example.com/cavs",
"events": ["repository.push", "release.published"],
"active": true,
"consecutive_failures": 0,
"secret": "whsec_9dK2…",
"created_at": "2026-07-28T12:00:00Z"
}
}

Requires organization.updateADMIN or OWNER.

Only these nine leave the platform. Everything else is internal.

Event Fires when
repository.created A repository is created
repository.deleted A repository is deleted
repository.push An upload session is finalized
repository.push.indexed A Git-index upload is finalized
release.published A release appears from a tag
storage.snapshot.created A snapshot capture completes
storage.quota.warning Storage crosses the warning threshold
storage.quota.exceeded Storage exceeds the plan limit
maintenance.gc.completed A garbage-collection pass completes

The full internal event list is in the Event catalog.

{
"type": "repository.push",
"organization_id": "0a11bb22-…",
"repository_id": "0f8c3d4e-…",
"occurred_at": "2026-07-28T12:00:04.512Z",
"data": {
"session_id": "3b1f…",
"generation": 42
}
}
Field Notes
type The public event name
organization_id Always present
repository_id Present for repository-scoped events
occurred_at RFC 3339 UTC
data Event-specific; treat it as open — new keys can appear
POST /cavs HTTP/1.1
Content-Type: application/json
User-Agent: CAVS-Node-Webhooks/1
X-CAVS-Event: repository.push
X-CAVS-Delivery: 4f3a1b2c-…
X-CAVS-Signature: sha256=9a1f8c…
Header Use
X-CAVS-Event Route without parsing the body
X-CAVS-Delivery Unique per delivery — use it to deduplicate
X-CAVS-Signature sha256=<hex HMAC-SHA256 of the raw body>
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.CAVS_WEBHOOK_SECRET;
// Raw body — express.json() would destroy the bytes we need to hash.
app.post('/cavs', express.raw({ type: 'application/json' }), (req, res) => {
const received = req.get('X-CAVS-Signature') ?? '';
const expected =
'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on the delivery id, then acknowledge immediately.
enqueue(req.get('X-CAVS-Delivery'), event);
res.status(204).end();
});
Property Value
Attempts 3 per event
Backoff 500 ms, then 1 s (exponential from a 500 ms base)
Per-attempt timeout 10 seconds
Success any 2xx
Delivery order not guaranteed
Duplicates possible — deduplicate on X-CAVS-Delivery
Failures logged as a delivery record; never retried beyond the third attempt

Your endpoint must therefore:

  • Respond fast. Acknowledge with 204 and process asynchronously. Ten seconds is the whole budget, including your work.
  • Be idempotent. Same delivery id twice must be a no-op the second time.
  • Not assume order. Two pushes can arrive out of order. Reconcile against the API if order matters.

After 10 consecutive failures a webhook is automatically disabled. The organization gets an in-app notification and an email.

Terminal window
# Check health
curl -sS "https://cavsnode.com/api/v1/organizations/acme-ai/webhooks" \
-H "Authorization: Bearer $CAVS_TOKEN" \
| jq '.webhooks[] | {url, active, consecutive_failures, last_error, last_delivery_at}'
# Re-enable after fixing the endpoint — this also clears the breaker
curl -sS -X PATCH "https://cavsnode.com/api/v1/organizations/acme-ai/webhooks/$WH" \
-H "Authorization: Bearer $CAVS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"active": true}'

A single success resets the counter.

Terminal window
curl -sS "https://cavsnode.com/api/v1/organizations/acme-ai/webhooks/$WH/deliveries" \
-H "Authorization: Bearer $CAVS_TOKEN" | jq

Each record: event type, success, HTTP status, attempt count, error, duration and timestamp. Delivery records are retained on a TTL, so this is a debugging aid — not an archive.

Terminal window
export API=https://cavsnode.com/api/v1
export H="Authorization: Bearer $CAVS_TOKEN"
curl -sS "$API/organizations/acme-ai/webhooks" -H "$H"
# Change the URL, the subscription set, or pause it
curl -sS -X PATCH "$API/organizations/acme-ai/webhooks/$WH" \
-H "$H" -H "Content-Type: application/json" \
-d '{"events": ["repository.push", "release.published", "storage.quota.exceeded"]}'
curl -sS -X PATCH "$API/organizations/acme-ai/webhooks/$WH" \
-H "$H" -H "Content-Type: application/json" -d '{"active": false}'
curl -sS -X DELETE "$API/organizations/acme-ai/webhooks/$WH" -H "$H"

Trigger a deploy on release.

if (event.type === 'release.published') {
await triggerDeploy({ tag: event.data.tag, repo: event.repository_id });
}

Kick off a downstream job when data lands.

if (event.type === 'repository.push') {
const objects = await cavsApi(`/repositories/${event.repository_id}/objects?sort=recent&limit=10`);
await enqueueTraining(objects);
}

Page on a quota breach.

if (event.type === 'storage.quota.exceeded') {
await pagerduty.trigger({
summary: `CAVS storage quota exceeded for org ${event.organization_id}`,
severity: 'warning',
});
}

Your endpoint must be publicly reachable. Use a tunnel:

Terminal window
ngrok http 3000
# then point the webhook at https://<subdomain>.ngrok.io/cavs

Or capture and replay: log the raw body and headers of one real delivery, then curl them at your local server while you iterate.

  • ✅ Verify X-CAVS-Signature on every request, in constant time.
  • ✅ Hash the raw body, before any parsing.
  • ✅ Use https and a valid certificate.
  • ✅ Deduplicate on X-CAVS-Delivery.
  • ✅ Keep the secret in a secret manager, and rotate by recreating the webhook.
  • ❌ Don’t trust data fields for authorization decisions — re-read from the API.
  • ❌ Don’t do slow work inline; you have 10 seconds.
  • ❌ Don’t log the raw body indefinitely if payloads may carry sensitive names.