Skip to main content
DocsAPI ReferenceAgents

Webhooks & notifications

Get notified by email or a signed webhook when your AI visibility changes.

View Markdown

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-Typevisibility.alert (or visibility.test from the test button).
  • X-Trackee-Timestamp — unix seconds when the event was signed.
  • X-Trackee-Signaturesha256=<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 fresh

Test 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.

On this page