# Webhooks & notifications (/docs/webhooks)



Trackee watches your brand's visibility with [trackers](/docs/api). 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 [#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:

```bash
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:

```bash
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 [#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 [#webhook-payload]

Trackee POSTs JSON to your `webhookUrl`. A run can detect several changes, so alerts arrive as an array.

```json
{
  "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` (or `visibility.test` from 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 [#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.

```js
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;
}
```

```python
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 [#test-it]

Send a sample alert to a notification to verify it works:

```bash
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.
