> ## Documentation Index
> Fetch the complete documentation index at: https://docs.querying.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Monitor Events and Alerts: Webhooks and Email Notifications

> Receive a signed webhook when a monitor's answers change, and an email when your mention rate falls below a threshold.

A monitor can tell you when something moves, so you do not have to poll its report:

* **Change events** go to your `webhookUrl` when a prompt's answers change: your brand appears or
  disappears, a competitor pulls ahead, or a site is cited for the first time.
* **Alerts** email your account's admins when the brand's mention rate over the last seven days falls
  below `alertBelowPct`.

## Change events

Set `webhookUrl` when you [create](/monitors.md#create-a-monitor) or update a monitor:

```bash theme={"theme":"css-variables"}
curl -X PATCH https://api.querying.ai/v1/monitors/$MONITOR_ID \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "webhookUrl": "https://hooks.example.com/querying/monitors" }'
```

The URL must be HTTPS on a public host. Set it with an API key: events are signed with the key that set
it, so a request without a key (a dashboard session) returns `400 VALIDATION_ERROR`. `webhookUrl: null`
turns events off and discards the monitor's event state; turning them on again starts from its history.

### Event types

| Event | Sent when |
| - | - |
| `monitor.mention.started` | Your brand is in at least three of the last five answers to a prompt, after being in fewer. |
| `monitor.mention.stopped` | Your brand drops to two or fewer of the last five answers to a prompt. |
| `monitor.competitor.ahead` | A competitor is in at least three of the last five answers to a prompt, and in more of them than your brand. |
| `monitor.source.first_cited` | An answer cites a domain that the monitor's answers have never cited before. |

Brand events follow each prompt separately on each engine and, on a grid monitor, at each point. The state
is read from the last five answers, so `samples: 5` gives a full state in one run.

* An event is sent once, when the state changes. A brand that stays out of the answers sends one
  `monitor.mention.stopped` and nothing more until it returns.
* A prompt has a state once it has five answers. That first state is the baseline and sends nothing.
* A category monitor has no brand of its own and sends only `monitor.source.first_cited`.

When events start:

* **Turning on `webhookUrl` for a monitor that already has answers**: each prompt's state is rebuilt from
  its last five answers of the past 30 days, and every domain already cited counts as known. Events start
  with the next answer.
* **Creating a monitor with `webhookUrl`, or turning it on before any answers exist**: the first run sets
  the known domains, and `monitor.source.first_cited` counts answers from tasks created after that run is
  fully submitted. Brand events start once a prompt has five answers.

### Payload

Each event is a `POST` with a JSON body. For example:

```json theme={"theme":"css-variables"}
{
  "event": "monitor.mention.stopped",
  "id": "fd61d8f6-ebdf-4162-8b55-fd052c8502a9",
  "createdAt": "2026-10-10T03:12:44.120Z",
  "monitor": { "id": "d39b8de0-b159-4229-bf58-7064e5369db1", "name": "Earbud brand tracking" },
  "data": {
    "engine": "CHATGPT",
    "prompt": "best wireless earbuds for commuting",
    "location": null,
    "taskId": "863edb44-fc62-40dd-a5be-2af9409d59b6",
    "answers": 5,
    "mentions": 2
  }
}
```

| Field | Meaning |
| - | - |
| `event` | The event type. |
| `id` | The event id, also sent as the `querying-webhook-id` header. Deduplicate on it. |
| `monitor` | The monitor's `id` and `name`. |
| `data.engine`, `data.prompt` | The engine and prompt the answer belongs to. |
| `data.location` | `{ latitude, longitude }` of the answer's point, or `null` without a location. |
| `data.taskId` | The answer that caused the event. Open it with [`GET /v1/monitors/{id}/answers/{taskId}`](/monitors/evidence#read-one-answer). |
| `data.answers`, `data.mentions` | Answers in the state (5) and how many named your brand. Mention and competitor events only. |
| `data.competitor`, `data.competitorMentions` | The competitor and how many of the answers named it. `monitor.competitor.ahead` only. |
| `data.domain`, `data.url`, `data.label` | The newly cited domain, the cited page and its label. `monitor.source.first_cited` only, in place of the counts. |

### Signature and retries

Events are signed and retried like task webhooks: the same three `querying-webhook-*` headers, the same
HMAC-SHA256 scheme and the same retry schedule. Verify them with the code in
[Webhooks](/concepts/webhooks.md#verify-the-signature). Two things differ:

* `querying-webhook-id` is the event `id`. Deduplicate on it.
* The HMAC key is the SHA-256 digest of the API key that set `webhookUrl`, not of a task's key.

After you rotate the key, send `webhookUrl` again with the new key, and events are signed with it. If
the key that set `webhookUrl` is revoked or expires, events stop: each one is marked `failed` with the
reason in `delivery.lastError`, and nothing is sent unsigned.

### List and redeliver events

`GET /v1/monitors/{id}/events` ([reference](/api-reference/monitors/list-events.md)) lists events newest first.

| Parameter | Description |
| - | - |
| `limit` | 1–100, default 50. |
| `type` | One event type. |
| `cursor` | The previous page's `nextCursor`. |

The response has `webhookUrl`, `nextCursor` (`null` on the last page) and `events`. Each event has `id`,
`type`, `createdAt`, `data` (as in the payload) and `delivery`:

| Field | Meaning |
| - | - |
| `delivery.status` | `pending` (waiting for a first attempt or a retry), `delivered` or `failed` (retries exhausted, or undeliverable). |
| `delivery.attempts` | Attempts made. |
| `delivery.deliveredAt` | When a `2xx` was received, or `null`. |
| `delivery.lastError` | The last failure, such as `HTTP 500`, or `null`. |

`POST /v1/monitors/{id}/events/{eventId}/redeliver`
([reference](/api-reference/monitors/redeliver-event.md)) sends one event again now to the current
`webhookUrl`, with a fresh set of retries, and returns the event with its updated `delivery`. Delivered
events can be sent again too. It returns `409 WEBHOOK_NOT_SET` when the monitor has no `webhookUrl` and
`404 NOT_FOUND` for an unknown event.

Events are kept for 30 days. An event still being retried is kept until its retries end.

## Alerts

Set `alertBelowPct` on a brand monitor, a whole percentage from 1 to 100, to get an email when the
brand's mention rate falls below it:

```bash theme={"theme":"css-variables"}
curl -X PATCH https://api.querying.ai/v1/monitors/$MONITOR_ID \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "alertBelowPct": 40 }'
```

How the alert is decided:

* **Window**: the scored answers of the last 7 days, across all of the monitor's prompts and engines. The
  [report](/monitors/reports.md) defaults to 30 days, which moves too slowly to warn anyone.
* **When**: each time a scheduled run starts. The check reads the answers already scored, so it reflects
  the previous runs.
* **Minimum sample**: fewer than 5 scored answers in the window never fires. A new monitor, or one whose
  engines are failing, stays quiet.
* **Crossing, not state**: the alert fires when the rate is below the threshold and the alert is armed,
  then disarms. It re-arms once the rate is back at or above the threshold, so a monitor that stays low
  emails once, not every run. Changing `alertBelowPct` re-arms it.

The email goes to every admin of the account that pays for the monitor (for a sub-account, its parent
account). It states the rounded rate, the threshold, the window and the number of scored answers, and
links to the monitor in the dashboard.

A category monitor has no brand rate, so it rejects `alertBelowPct` with `400 VALIDATION_ERROR`.
`alertBelowPct: null` turns the alert off.

### Alert history

`GET /v1/monitors/{id}/alerts` ([reference](/api-reference/monitors/get-alerts.md)) returns
`alertBelowPct`, `armed` and `deliveries`: the 20 most recent alert emails for the monitor, tests
included. Each delivery has `id`, `status` (`pending`, `sent` or `failed`), `attempts`, `createdAt`,
`sentAt`, `lastError` and `payload` (`monitorId`, `name`, `ratePct`, `threshold`, `windowDays`, `runs`).
Delivery records are kept for 90 days.

### Send a test alert

`POST /v1/monitors/{id}/alerts/test` ([reference](/api-reference/monitors/test-alert.md)) queues a test email
through the same delivery path as a real alert, with "(test)" after the monitor name, and returns
`data.queued: true`. Check the result in the alert history. You can send three test emails per monitor
every five minutes; more returns `429 RATE_LIMITED`. A category monitor returns `400 VALIDATION_ERROR`.

## Next steps

> **[Webhooks](/concepts/webhooks.md)**
>
> Signing, retries and safe handlers for every webhook.

> **[Sources and answers](/monitors/evidence.md)**
>
> Open the answer behind an event.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.