Webhooks
Get an HTTPS POST when something changes, such as a system becoming high-risk or an incident being logged, instead of polling.
Events
| Event | When |
|---|---|
system.created | An AI system was added to the inventory. |
system.tier_changed | An AI system's EU AI Act risk tier changed. |
evidence.added | An evidence file was uploaded. |
finding.created | A finding was opened. |
incident.created | An incident was logged. |
proof.sealed | A Proof Seal was created and timestamped. |
passport.issued | An AI Conformity Passport was issued. |
passport.revoked | An AI Conformity Passport was revoked. |
discovery.new_asset | Discovery found an AI service or model that wasn't seen before. |
Create an endpoint
In the app (Settings → Webhooks) or with an admin key. The signing secret is returned once.
curl https://v77trust.com/api/v1/webhook-endpoints \
-H "Authorization: Bearer $V77_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://grc.example.eu/hooks/v77", "events": ["system.tier_changed", "incident.created", "finding.created"] }'The request you receive
POST /hooks/v77 HTTP/1.1
Content-Type: application/json
User-Agent: V77-Trust-Webhooks/1
X-V77-Event: system.tier_changed
X-V77-Delivery: 3f6c0d1e-…
X-V77-Signature: t=1762594200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
"id": "3f6c0d1e-…",
"event": "system.tier_changed",
"createdAt": "2026-11-08T09:30:00.000Z",
"orgId": "…",
"data": { "systemId": "…", "name": "Retail credit scoring", "previousTier": "limited", "riskTier": "high_risk" }
}Verify the signature
X-V77-Signature is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body> keyed with your endpoint secret. Check it against the raw body (before JSON parsing), compare in constant time, and reject timestamps older than 5 minutes to stop replays.
import crypto from "node:crypto";
export function verifyV77(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: use the raw body
app.post("/hooks/v77", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyV77(req.body.toString("utf8"), req.get("X-V77-Signature"), process.env.V77_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
res.sendStatus(204); // acknowledge fast, process async
queue.push(event);
});Delivery
- Respond with any 2xx within 5 seconds. Do heavy work after responding.
- Deliveries are attempted once and aren't retried automatically. Each attempt and its status is logged in Settings → Webhooks. If you can't miss an event, also reconcile periodically with the list endpoints or
GET /audit-log. - The same event may occasionally arrive twice; deduplicate on
id. - Endpoints must be public HTTPS URLs; private and local addresses are refused.
Treat webhooks as a signal: for critical decisions, fetch the current object from the API by id rather than trusting the payload alone.