Webhooks

Receive signed HTTPS POSTs when AI coding tools ship releases, breaking changes or security fixes. Events, payloads, HMAC-SHA256 signature verification in Node and Python, retries and endpoint rules.

Webhooks need an API key and run on api.havoptic.com. Havoptic POSTs a signed JSON event to your HTTPS endpoint shortly after a release is detected and enriched.

Events

EventFires when
release.createdA new release is detected and enriched. data.release carries id, version, abstract, counts and an api link.
change.breakingA release has a breaking change item with confidence ≥ 0.85.
change.securityA release has a security change item with confidence ≥ 0.85.

change.* events carry data.release plus data.changes, the matching items. release.updated and product.added can be subscribed to already but are not emitted yet. POST /v1/webhooks/{id}/test sends a ping event.

Register an endpoint

curl -X POST https://api.havoptic.com/v1/webhooks \
  -H "Authorization: Bearer $HAVOPTIC_API_KEY" -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/hooks/havoptic","events":["change.breaking","change.security"],"filters":{"tools":["claude-code","cursor"]}}'
# → { "id": "…", "url": "…", "events": […], "filters": {…}, "secret": "whsec_…" }

The secret is shown only once; store it. filters.tools and filters.kinds are optional (empty means all).

RequestPurpose
GET /v1/webhooksList your webhooks.
POST /v1/webhooksCreate one.
DELETE /v1/webhooks/{id}Delete one.
POST /v1/webhooks/{id}/testSend a test delivery now.
GET /v1/webhooks/{id}/deliveriesRecent deliveries with status and attempts.

Developer keys can register 3 webhooks (webhook_limit_reached beyond that); Pro and Team are unlimited.

Payload and headers

POST /hooks/havoptic HTTP/1.1
Content-Type: application/json
Havoptic-Event: change.breaking
Havoptic-Delivery: whd_…
Havoptic-Signature: t=1760870400,v1=5d41402abc4b2a76b9719d911017c592…

{
  "id": "whd_…",
  "type": "change.breaking",
  "created_at": "2026-10-19T10:15:00.000Z",
  "data": {
    "release": { "id": "claude-code-2.1.302", "tool": "claude-code", "version": "2.1.302", "has_breaking": true, "api": "https://api.havoptic.com/v1/releases/claude-code-2.1.302?expand=changes" },
    "changes": [ { "kind": "breaking", "title": "…", "evidence": "…", "confidence": 0.93 } ]
  }
}

Verify the signature

v1 is HMAC_SHA256(secret, "<t>.<raw body>") in hex. Verify against the raw request body (before JSON parsing), compare in constant time, and reject timestamps older than 5 minutes to stop replays.

Node.js

// Node 18+
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyHavoptic(rawBody, header, secret, toleranceSec = 300) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

// Express: keep the raw body, verify, then parse.
app.post('/hooks/havoptic', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyHavoptic(req.body.toString('utf8'), req.get('Havoptic-Signature') || '', process.env.HAVOPTIC_WEBHOOK_SECRET);
  if (!ok) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  res.sendStatus(204); // answer fast, process async
  handle(event);
});

Python

import hashlib, hmac, time

def verify_havoptic(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

# Flask
@app.post("/hooks/havoptic")
def havoptic_hook():
    if not verify_havoptic(request.get_data(), request.headers.get("Havoptic-Signature", ""), SECRET):
        abort(400)
    event = request.get_json()
    return "", 204

Delivery and retries

Endpoint rules (SSRF protection)

Developer docs · API reference · OpenAPI · Havoptic