Register an HTTPS endpoint with the /api/v2/public/webhooks/subscribe call below. When a subscribed event fires we POST a signed JSON envelope to your target_url. Every delivery carries these headers:
x-gigi-signature: t=<unix>,v1=<hex> — the HMAC signature.x-gigi-timestamp — the same unix timestamp.x-gigi-event, x-gigi-event-id, x-gigi-delivery-id — event type, stable event id (idempotency key), per-attempt delivery id.
Verify every delivery: compute HMAC-SHA256(secret, "<t>.<raw_body>") over the raw request body, constant-time compare against v1, and reject deliveries whose t is outside your tolerance (5 minutes recommended). Return any 2xx to acknowledge — a non-2xx or timeout triggers up to 3 retries with exponential backoff.
Envelope
{
"id": "evt_9c2f…",
"type": "sentinel.fired",
"account_id": "acc_9a…",
"created_at": "2026-07-03T12:00:00.000Z",
"data": {
"firing_id": "fir_demo_01",
"sentinel_name": "missed_followup_72h",
"triggered_at": "2026-07-03T12:00:00.000Z",
"action_taken": "notify_agent"
}
}
Node (Express)
const crypto = require("crypto");
// Verify a GiGi webhook delivery. Pass the RAW (unparsed) request body.
function verifyGigiWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
// signatureHeader looks like: "t=1720008000,v1=9f86d0..."
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => {
const i = kv.indexOf("=");
return [kv.slice(0, i), kv.slice(i + 1)];
}),
);
const t = Number(parts.t);
const provided = parts.v1;
if (!t || !provided) return false;
// Replay protection: reject deliveries outside your tolerance window.
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(provided);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: capture the raw body so the signature matches byte-for-byte.
const express = require("express");
const app = express();
app.post(
"/hooks/gigi",
express.raw({ type: "application/json" }),
(req, res) => {
const ok = verifyGigiWebhook(
req.body.toString("utf8"),
req.get("x-gigi-signature") || "",
process.env.GIGI_WEBHOOK_SECRET,
);
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
console.log("verified", event.type, event.id);
res.sendStatus(200); // 2xx acknowledges; non-2xx triggers a retry.
},
);
Python (Flask)
import hashlib, hmac, time, json
from flask import Flask, request, abort
app = Flask(__name__)
def verify_gigi_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
# signature_header looks like: "t=1720008000,v1=9f86d0..."
try:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
t = int(parts["t"])
provided = parts["v1"]
except (KeyError, ValueError):
return False
# Replay protection: reject deliveries outside your tolerance window.
if abs(time.time() - t) > tolerance:
return False
signed = f"{t}.".encode("utf-8") + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, provided)
@app.post("/hooks/gigi")
def gigi_hook():
ok = verify_gigi_webhook(
request.get_data(), # raw bytes
request.headers.get("x-gigi-signature", ""),
app.config["GIGI_WEBHOOK_SECRET"],
)
if not ok:
abort(400)
event = json.loads(request.get_data())
print("verified", event["type"], event["id"])
return "", 200 # 2xx acknowledges; non-2xx triggers a retry.