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.
Creating one
Section titled “Creating one”-
Go to Organization settings → Webhooks → New webhook.
-
Enter your endpoint URL (
httporhttps; usehttps) and pick the events to subscribe to.*subscribes to everything. -
Copy the signing secret. CAVS generates it (prefixed
whsec_) and returns it exactly once.
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"] }'{ "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.update — ADMIN or OWNER.
Event types
Section titled “Event types”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.
Payload
Section titled “Payload”{ "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 |
Headers
Section titled “Headers”POST /cavs HTTP/1.1Content-Type: application/jsonUser-Agent: CAVS-Node-Webhooks/1X-CAVS-Event: repository.pushX-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> |
Verifying the signature
Section titled “Verifying the signature”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();});import hashlibimport hmacimport os
from flask import Flask, request
app = Flask(__name__)SECRET = os.environ["CAVS_WEBHOOK_SECRET"].encode()
@app.post("/cavs")def cavs_webhook(): received = request.headers.get("X-CAVS-Signature", "") expected = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected): return "bad signature", 401
event = request.get_json() enqueue(request.headers.get("X-CAVS-Delivery"), event) return "", 204func handler(secret []byte) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) if err != nil { http.Error(w, "bad request", http.StatusBadRequest) return }
mac := hmac.New(sha256.New, secret) mac.Write(body) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(r.Header.Get("X-CAVS-Signature")), []byte(expected)) { http.Error(w, "bad signature", http.StatusUnauthorized) return }
var ev struct { Type string `json:"type"` RepositoryID string `json:"repository_id"` Data map[string]any `json:"data"` } if err := json.Unmarshal(body, &ev); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } enqueue(r.Header.Get("X-CAVS-Delivery"), ev) w.WriteHeader(http.StatusNoContent) }}Delivery semantics
Section titled “Delivery semantics”| 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
204and 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.
The circuit breaker
Section titled “The circuit breaker”After 10 consecutive failures a webhook is automatically disabled. The organization gets an in-app notification and an email.
# Check healthcurl -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 breakercurl -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.
Inspecting deliveries
Section titled “Inspecting deliveries”curl -sS "https://cavsnode.com/api/v1/organizations/acme-ai/webhooks/$WH/deliveries" \ -H "Authorization: Bearer $CAVS_TOKEN" | jqEach 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.
Managing webhooks
Section titled “Managing webhooks”export API=https://cavsnode.com/api/v1export H="Authorization: Bearer $CAVS_TOKEN"
curl -sS "$API/organizations/acme-ai/webhooks" -H "$H"
# Change the URL, the subscription set, or pause itcurl -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"Recipes
Section titled “Recipes”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', });}Local development
Section titled “Local development”Your endpoint must be publicly reachable. Use a tunnel:
ngrok http 3000# then point the webhook at https://<subdomain>.ngrok.io/cavsOr capture and replay: log the raw body and headers of one real delivery, then
curl them at your local server while you iterate.
Security checklist
Section titled “Security checklist”- ✅ Verify
X-CAVS-Signatureon every request, in constant time. - ✅ Hash the raw body, before any parsing.
- ✅ Use
httpsand a valid certificate. - ✅ Deduplicate on
X-CAVS-Delivery. - ✅ Keep the secret in a secret manager, and rotate by recreating the webhook.
- ❌ Don’t trust
datafields 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.
- Event catalog — every event, public and internal.
- Notifications & activity — the other notification channels.
- Organizations API — the management endpoints.

