Webhooks & notifications
Get notified by email or a signed webhook when your AI visibility changes.
Trackee watches your brand's visibility with trackers. When a scheduled run detects a change, Trackee records an alert and can notify you two ways: by email, and by a signed webhook to your own endpoint.
Configure notifications
Create as many notifications as you want in the dashboard under Settings, Notifications, or through the API. Each notification is one channel (email or webhook), the event types it cares about, and an optional brand scope.
Create an email notification with one or more recipients:
curl -X POST https://api.trackee.dev/v1/notifications \
-H "x-access-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"channel": "email",
"emailRecipients": ["alice@acme.com", "ops@acme.com"],
"eventTypes": ["mention_lost", "position_changed", "competitor_new"]
}'Create a webhook notification:
curl -X POST https://api.trackee.dev/v1/notifications \
-H "x-access-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"channel": "webhook",
"webhookUrl": "https://acme.com/hooks/trackee",
"eventTypes": ["mention_lost", "mention_gained"]
}'A webhook notification is created with a signing secret (whsec_...) in the response. List them with GET /v1/notifications, update one with PATCH /v1/notifications/{id}, delete with DELETE /v1/notifications/{id}, and rotate a secret with POST /v1/notifications/{id}/rotate-secret. Omit eventTypes to receive every change type, and add "brandId" to scope a notification to a single brand.
Alert types
eventTypes controls which changes are delivered:
mention_gained— your brand started being mentioned for a prompt.mention_lost— your brand stopped being mentioned.position_changed— your position in the answer moved.competitor_new— a new competitor appeared alongside you.sentiment_changed— how an engine talks about you changed.
Webhook payload
Trackee POSTs JSON to your webhookUrl. A run can detect several changes, so alerts arrive as an array.
{
"id": "evt_9f1c2ab34d5e",
"type": "visibility.alert",
"created_at": "2026-09-07T12:00:00.000Z",
"organization_id": "6a1b...",
"data": {
"alerts": [
{
"type": "position_changed",
"brand": "Acme",
"brandId": "6a2c...",
"trackerId": "6a3d...",
"prompt": "best project management software",
"engine": "chatgpt",
"message": "Acme moved from position 5 to 2 on ChatGPT."
}
]
}
}Each request carries these headers:
X-Trackee-Event-Id— unique event id. Deduplicate on this, since deliveries can retry.X-Trackee-Event-Type—visibility.alert(orvisibility.testfrom the test button).X-Trackee-Timestamp— unix seconds when the event was signed.X-Trackee-Signature—sha256=<hex>(see below).
Respond with any 2xx within 10 seconds to acknowledge.
Verify the signature
The signature is an HMAC-SHA256 of "{timestamp}.{rawBody}" using your signing secret. Compute it over the raw request body, before any JSON parsing, and compare in constant time. Reject timestamps older than a few minutes to prevent replays.
import crypto from "node:crypto";
// Express: app.post("/hooks/trackee", express.raw({ type: "application/json" }), ...)
function verify(req, secret) {
const timestamp = req.get("X-Trackee-Timestamp");
const signature = req.get("X-Trackee-Signature");
const raw = req.body; // Buffer, the exact bytes Trackee sent
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${raw}`)
.digest("hex");
const a = Buffer.from(signature ?? "");
const b = Buffer.from(expected);
// timingSafeEqual throws on length mismatch, so check length first.
const ok = a.length === b.length && crypto.timingSafeEqual(a, b);
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
return ok && fresh;
}import hmac, hashlib, time
def verify(headers, raw_body: bytes, secret: str) -> bool:
timestamp = headers["X-Trackee-Timestamp"]
signature = headers["X-Trackee-Signature"]
expected = "sha256=" + hmac.new(
secret.encode(),
f"{timestamp}.{raw_body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
fresh = abs(time.time() - int(timestamp)) < 300
return hmac.compare_digest(signature, expected) and freshTest it
Send a sample alert to a notification to verify it works:
curl -X POST https://api.trackee.dev/v1/notifications/{id}/test \
-H "x-access-key: YOUR_API_KEY"The response reports sent or failed for that notification.