> For the complete documentation index, see [llms.txt](https://docs.revenium.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.revenium.io/integrations/ai-insights-webhooks.md).

# AI Insights Webhooks

Subscribe a webhook to AI Insights events and react the moment a run finishes or someone actions a recommendation, instead of polling the Insights API on a timer.

Typical uses: post high-severity recommendations to Slack or PagerDuty, open a ticket for every finding above a severity threshold, or keep your own system in sync when anyone rates a recommendation or marks it as implemented.

## The four events

| Event                             | In Notification settings | Fires when                                                                                                  |
| --------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `insight_run.completed`           | Run completed            | A run finished and every investigator succeeded. Carries the finding and recommendation counts.             |
| `insight_run.partial`             | Run partial              | A run finished with results, but at least one investigator failed. Names which succeeded and which did not. |
| `insight_run.failed`              | Run failed               | A run reached a terminal failed state. Carries a `failure_reason`.                                          |
| `recommendation_feedback.created` | Feedback recorded        | Feedback was recorded on a recommendation, whether from the dashboard or the API.                           |

A run reports itself through exactly one of the three run events, and under normal delivery a single run is not reported as both finished and failed. Two silences are expected and worth designing around: a **cancelled** run emits nothing at all, and re-running an analysis that Revenium recognises as identical to a recent one emits nothing either, because it reuses that result instead of producing a new one. Treat the absence of an event as "no new result", never as "the run succeeded". "Delivery semantics" below covers the one case where two run events can arrive for the same `run_id`.

## Subscribing

Subscriptions are per channel and per event, so one webhook can take everything while another takes failures only.

1. **Create the webhook.** Open **Guardrails** under **Spend Management** in the sidebar, then the **Channels** tab, and use **Add Webhook** in the Webhooks section. Fill in **Name** and **URL**, and pick **HMAC signing** under Authentication. The signing secret is shown once — copy it. Signing is not a one-time decision: each webhook shows a **Signing enabled** or **Signing disabled** badge, and the **Enable Signing** button turns it on later (**Rotate Secret** once it is on).
2. **Pick the events.** Open **AI Insights** and press the bell button, **Notification settings**, in the toolbar above the run. Every active webhook in the team appears as a row with a checkbox per event type. Check what that channel should receive and save.
3. **Verify.** See "Testing" below.

Unchecking an event stops delivery for that channel immediately; other channels are unaffected. Deleting a webhook removes its subscriptions with it.

## The envelope

Every event shares the same seven top-level fields.

| Field            | Meaning                                                                                            |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| `event_type`     | One of the four tokens above.                                                                      |
| `run_id`         | The insights run the event belongs to. Feedback events carry the run the recommendation came from. |
| `event_id`       | Unique per event, stable across retries of the same event.                                         |
| `schema_version` | `v1` today. A breaking payload change ships a new version.                                         |
| `occurred_at`    | ISO-8601 UTC timestamp of the event, not of the delivery attempt. Unchanged across retries.        |
| `livemode`       | `false` for non-production environments, `true` in production.                                     |
| `data`           | Event-specific payload, described below.                                                           |

```json
{
  "event_type": "insight_run.completed",
  "run_id": "9f14b0c2-6f5a-4c1e-9b7d-2a0e5c8d3f41",
  "event_id": "evt_5c9a1d84-3b2f-4e07-8a6c-1d9f7b0e2c53",
  "schema_version": "v1",
  "occurred_at": "2026-08-04T15:38:36.765Z",
  "livemode": true,
  "data": {
    "team_id": 1234567890,
    "status": "completed",
    "triggered_by": "user",
    "period_start": "2026-07-05T15:37:11.075Z",
    "period_end": "2026-08-04T15:37:11.075Z",
    "findings_count": 14,
    "recommendations_count": 4,
    "failed_investigators": [],
    "severity_histogram": { "critical": 1, "high": 1, "medium": 2, "low": 0, "info": 0 },
    "total_monthly_impact_usd": 2031.32,
    "findings_truncated": false,
    "total_duration_ms": 82812,
    "dashboard_url": "https://app.revenium.ai/costs-revenue/insights?runId=9f14b0c2-6f5a-4c1e-9b7d-2a0e5c8d3f41"
  }
}
```

## Payloads

### `insight_run.completed`

| Field                        | Type      | Notes                                                                                                                                                       |
| ---------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `team_id`                    | number    | The team the run belongs to.                                                                                                                                |
| `status`                     | string    | Always `completed`.                                                                                                                                         |
| `triggered_by`               | string    | `user`, `api`, `system` or `schedule`.                                                                                                                      |
| `period_start`, `period_end` | string    | The analysed window, ISO-8601 UTC.                                                                                                                          |
| `findings_count`             | number    | Findings produced. `0` is normal: findings below the economic-value floor are filtered out.                                                                 |
| `recommendations_count`      | number    | Recommendations produced. Can be `0` on a successful run with findings, when the recommendation step degraded and the findings were preserved on their own. |
| `failed_investigators`       | string\[] | Always empty here: a run is only `completed` when every investigator succeeded.                                                                             |
| `severity_histogram`         | object    | Counts by `critical`, `high`, `medium`, `low`, `info`.                                                                                                      |
| `total_monthly_impact_usd`   | number    | Estimated monthly impact across the findings.                                                                                                               |
| `findings_truncated`         | boolean   | `true` when the finding list was capped.                                                                                                                    |
| `total_duration_ms`          | number    | Wall-clock duration of the run.                                                                                                                             |
| `dashboard_url`              | string    | Deep link to this run in the dashboard.                                                                                                                     |

### `insight_run.partial`

Same fields as `completed`, with `status` set to `partial`, `failed_investigators` non-empty, and one addition:

| Field                     | Type      | Notes                                                                     |
| ------------------------- | --------- | ------------------------------------------------------------------------- |
| `succeeded_investigators` | string\[] | Which investigators did produce results. The histogram covers only these. |

### `insight_run.failed`

Carries `team_id`, `triggered_by`, `period_start`, `period_end`, `total_duration_ms` and `dashboard_url` in the same positions as `completed`, plus:

| Field                      | Type      | Notes                                                                                                          |
| -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------- |
| `status`                   | string    | Always `failed`.                                                                                               |
| `failure_reason`           | string    | `analysis_failed`, `dispatch_failed`, `missing_config`, `invalid_payload` or `timed_out`.                      |
| `error_summary`            | string    | Short cause, safe to show to a human.                                                                          |
| `failed_investigators`     | string\[] | **Always empty on this event.** A failed run reports its cause through `failure_reason`, not per investigator. |
| `severity_histogram`       | object    | All zeros.                                                                                                     |
| `total_monthly_impact_usd` | number    | `0`.                                                                                                           |

Two shapes to expect on this event and not on the others: `total_duration_ms` is `0` when the run failed before it could be timed, and `period_start` / `period_end` fall back to the **epoch sentinel** `1970-01-01T00:00:00.000Z` when the failure happened before the requested window was validated. Guard against that before rendering the window to a person.

### `recommendation_feedback.created`

| Field                          | Type           | Notes                                                                                                        |
| ------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------ |
| `team_id`                      | number         | The team the recommendation belongs to.                                                                      |
| `recommendation_id`            | string         | The recommendation the feedback is about.                                                                    |
| `action`                       | string         | One of `acknowledged`, `dismissed`, `implemented`, `already_aware`, `not_applicable`. See the mapping below. |
| `confidence_rating`            | number         | `1`, `0` or `-1`. Derived from the action, not entered by the person.                                        |
| `dismissal_reason`             | string         | Free text, only ever populated by the thumbs-down path, and optional there.                                  |
| `realized_savings`             | number         | Reported savings. The dashboard never sets it, so it is `0` unless the feedback came through the API.        |
| `realized_savings_currency`    | string         | ISO currency code, `USD` by default.                                                                         |
| `realized_savings_measured_at` | string \| null | When the savings were measured, `null` unless reported through the API.                                      |
| `actor_type`                   | string         | `user` for the dashboard, `api` for an API caller.                                                           |

Feedback events are **not** deduplicated: two pieces of feedback on the same run produce two events, which is what keeps multiple recommendations distinguishable.

**What each dashboard control sends.** A recommendation card carries two independent controls: an accuracy vote (the thumbs) and a status note (the overflow menu). A recommendation can therefore have both, recorded as separate events.

| Control on the recommendation            | `action`         | `confidence_rating` |
| ---------------------------------------- | ---------------- | ------------------- |
| Thumbs up, under "Accurate?"             | `acknowledged`   | `1`                 |
| Thumbs down, with an optional "why" note | `dismissed`      | `-1`                |
| Overflow menu, "Mark as implemented"     | `implemented`    | `0`                 |
| Overflow menu, "Already aware"           | `already_aware`  | `0`                 |
| Overflow menu, "Not applicable"          | `not_applicable` | `-1`                |

Read `action` rather than the rating when you care about intent: `-1` covers both "this was inaccurate" and "this does not apply to us", and `0` covers a recommendation that was implemented as well as one that was already known.

## Delivery semantics

* **Best effort, not guaranteed.** Revenium retries a transient failure and then stops, so an event can be lost.
* **Retries.** A `5xx` or a network error is retried up to three attempts, roughly 10s then 20s apart. A `4xx` is terminal and is not retried.
* **Retries are the same event.** `event_id`, `occurred_at` and the signature headers are identical across attempts, so an endpoint that received an attempt without answering in time will see the same event again.
* **Terminal run events are deduplicated** per run and event type, so a re-processed run does not fire the same terminal event twice.
* **If a run is ever retried at the infrastructure level, you can receive both a success and a failure for one `run_id`.** Treat `completed` or `partial` as authoritative over `failed`.
* **Ordering is not guaranteed** across channels or events. Use `occurred_at`.

## Verifying deliveries

Enable HMAC on the webhook and verify the `X-Revenium-Signature-256` header on every request. The algorithm, the 24-hour rotation overlap, the 300-second replay window and ready-to-use verifiers in Node, Python and Go are documented in [Webhook Signing](/integrations/webhook-signing.md).

## Testing

Two ways, both useful:

* **Synthetic delivery.** Press **Test** on the webhook, on the **Channels** tab. It sends a test payload through the same signing and delivery path as a real event, so it exercises your endpoint and your verifier without waiting for a run. The API equivalent, and what a correct verifier looks like, are in [Webhook Signing](/integrations/webhook-signing.md).
* **A real run.** Subscribe one channel to `insight_run.completed` only, trigger a run from the Insights page, and confirm that exactly one delivery arrives and that no other event type does.

## Recommended consumer behavior

1. **Answer `2xx` before you do the work.** Acknowledge the delivery, then process asynchronously. A slow handler burns the retry budget, and a `4xx` from your side is never retried.
2. **Deduplicate on `event_id`.** A retry is the same event, not a new one, so keep a short window of processed identifiers rather than trusting one-shot arrival.
3. **Treat the event as a trigger, not as the record.** Read the run back through the Insights API when you need the full findings or recommendations; the payload is a summary by design.
4. **Subscribe the narrowest set of events per channel.** An on-call destination usually wants `insight_run.failed` alone, while a dashboard integration wants the terminal events but not feedback.
5. **Handle `partial` explicitly.** It carries results and a list of investigators that failed, so a consumer that treats it as a plain success will report numbers built from an incomplete analysis.

If you need an event type that is not listed here, or delivery guarantees beyond at-most-once, [contact support](mailto:support@revenium.io).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.revenium.io/integrations/ai-insights-webhooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
