# Webhook event reference

> Request headers, CloudEvents body, rule outcomes, size limits, retry schedule, and endpoint requirements for the data quality webhook channel.

This page specifies what an endpoint registered as a [webhook alert channel](webhook-alerts.md) receives: the HTTP request, the event body, how rule results map to outcomes, the limits applied to large runs, and how deliveries are retried.

## Request

Each delivery is a `POST` with a JSON body. Header names are case-insensitive and are sent in lowercase.

```http
POST /hooks/peak HTTP/1.1
content-type: application/json
user-agent: Peak-Webhooks/1.0
webhook-id: evt_1f0c6b2a9d3e4c5f8a7b6c5d4e3f2a1b
webhook-timestamp: 1791191520
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
x-peak-event-type: ai.peak.data_validation.run.completed
x-env: prod
```

| Header | Sent | Value |
|--------|------|-------|
| `content-type` | Always | `application/json` |
| `user-agent` | Always | `Peak-Webhooks/1.0` |
| `webhook-id` | Always | The event `id`. Identical on every retry of the same event, so it is the key to deduplicate on. |
| `webhook-timestamp` | Always | Unix time in seconds at which this attempt was sent. Fresh on every retry. |
| `webhook-signature` | `auth.type` is `hmac` | `v1,` followed by the base64 HMAC-SHA256 of `<webhook-id>.<webhook-timestamp>.<body>`. Recomputed on every retry. See [Verify the signature](webhook-alerts.md#verify-the-signature). |
| `x-peak-event-type` | Always | The event `type`, for routing before the body is parsed. |
| `authorization` | `auth.type` is `bearer-token` or `basic` | `Bearer <token>`, or `Basic <base64 of username:password>`. |
| `<headerName>` | `auth.type` is `api-key` | The key, in the header named by `auth.headerName` (default `X-API-Key`). |
| Static headers | When the channel defines `headers` | The configured names and values, as given. |

## Event body

The body is a [CloudEvents 1.0](https://cloudevents.io/) envelope in structured JSON mode. One event is sent per validation run per channel, carrying the rules of that run which name the channel, narrowed to the outcomes the channel subscribes to.

In this example three rules on `stage.orders` name the channel, which subscribes to every outcome. The freshness check failed, the missing-data check produced a warning, and the foreign-key check passed:

```json
{
  "specversion": "1.0",
  "id": "evt_1f0c6b2a9d3e4c5f8a7b6c5d4e3f2a1b",
  "type": "ai.peak.data_validation.run.completed",
  "source": "/peak/prod/data-validation/acme",
  "subject": "orders-pipeline/stage.orders",
  "time": "2026-10-05T09:12:00.123Z",
  "datacontenttype": "application/json",
  "data": {
    "tenant": { "name": "acme", "id": "1234" },
    "run": { "id": "e3f1c2a4-9b77-4d21-8c5e-1a2b3c4d5e6f" },
    "object": {
      "solution": "orders-pipeline",
      "table": "stage.orders",
      "warehouseType": "snowflake"
    },
    "outcome": "failed",
    "matched": [
      "ai.peak.data_validation.rule.failed",
      "ai.peak.data_validation.rule.passed",
      "ai.peak.data_validation.rule.warned"
    ],
    "summary": { "total": 3, "errored": 0, "failed": 1, "warned": 1, "passed": 1, "skipped": 0 },
    "results": [
      {
        "rule": {
          "type": "freshness",
          "severity": "error",
          "column": "order_date",
          "params": { "column": "order_date", "threshold": 24, "unit": "hours" }
        },
        "outcome": "failed",
        "status": "failed",
        "message": "Data is stale: column 'order_date' is 50 hours old (threshold: 24 hours)",
        "observed": 50,
        "errorCodes": ["DI_E_24F01"],
        "details": {
          "last_seen": "2026-10-03 07:12:44.000",
          "threshold": { "value": 24, "unit": "hours" }
        }
      },
      {
        "rule": {
          "type": "missing_data_validation",
          "severity": "warn",
          "column": "order_date",
          "params": { "column": "order_date", "interval": 1, "unit": "days", "minRowCount": 100 }
        },
        "outcome": "warned",
        "status": "failed",
        "message": "Missing data: 2 period(s) of 1 days had fewer than 100 rows",
        "observed": 2,
        "errorCodes": ["DI_W_24M01"],
        "details": {
          "expected_min": 100,
          "slice": { "interval": 1, "unit": "days" },
          "window": { "start": "2026-09-01 00:00:00.000", "end": "2026-10-05 09:11:58.410" },
          "missing_periods": [
            { "start": "2026-10-03 00:00:00.000", "end": "2026-10-04 00:00:00.000", "actual_count": 41 },
            { "start": "2026-10-04 00:00:00.000", "end": "2026-10-05 00:00:00.000", "actual_count": 0 }
          ]
        }
      },
      {
        "rule": { "type": "async_foreign_key", "severity": "warn" },
        "outcome": "passed",
        "status": "passed"
      }
    ],
    "resultsTotal": 3,
    "resultsTruncated": false,
    "links": {
      "dashboard": "https://platform.peak.ai/canvas/assets",
      "run": "https://service.peak.ai/validation-api/api/v1/validations/run/e3f1c2a4-9b77-4d21-8c5e-1a2b3c4d5e6f",
      "docs": "https://docs.uipath.com/industry-department-solutions/automation-cloud/latest/supply-chain-retail-api-guide/webhook-event-reference"
    }
  }
}
```

### Envelope

| Field | Description |
|-------|-------------|
| `specversion` | Always `1.0`. |
| `id` | Unique per event: `evt_` followed by 32 hexadecimal characters. Equals the `webhook-id` header. Two channels notified for the same run receive different ids. |
| `type` | Always `ai.peak.data_validation.run.completed`. What happened is in `data.outcome` and `data.summary`. |
| `source` | `/peak/<environment>/data-validation/<tenant>`. `<environment>` is `prod` for production, so an endpoint fed by more than one environment can tell them apart. |
| `subject` | `<solution>/`. |
| `time` | When the run completed, in RFC 3339 format, UTC, with millisecond precision. |
| `datacontenttype` | Always `application/json`. |

### `data`

| Field | Description |
|-------|-------------|
| `tenant.name`, `tenant.id` | The tenant the run belongs to. `id` is a string. |
| `run.id` | The validation run. It is the same in every event sent for that run, whichever channel receives it, so it correlates them. The full report is available from [Check a validation run](object-level-validations.md#check-a-validation-run). |
| `object.solution`, `object.table`, `object.warehouseType` | The table that was validated and its warehouse, `snowflake` or `redshift`. |
| `outcome` | The worst outcome among the carried results, in the order `errored`, `failed`, `warned`, `passed`. |
| `matched` | The channel's subscribed outcomes that are present in the carried results, as `ai.peak.data_validation.rule.<outcome>` names, sorted. This is why the channel was notified. |
| `summary` | Counts of the carried results by outcome: `total`, `errored`, `failed`, `warned`, `passed`, `skipped`. Complete even when `results` is capped. Skipped rules are never carried, so `skipped` is `0`. |
| `results` | One entry per carried result, ordered `errored`, `failed`, `warned`, `passed`. Capped, see [Size limits](#size-limits). |
| `resultsTotal`, `resultsTruncated` | The number of carried results, and whether `results` holds fewer than that. |
| `links.dashboard`, `links.run`, `links.docs` | The Canvas asset list where the [Data Quality Dashboard](data-quality-dashboard.md) lives, the run report endpoint, and this documentation. For a spoke tenant, `links.run` is on the spoke's service host. |

### Result entry

| Field | Present | Description |
|-------|---------|-------------|
| `rule.type` | Always | `freshness`, `missing_data_validation`, or `async_foreign_key`. |
| `rule.severity` | Always | `error` or `warn`, from the validation entry. |
| `rule.column` | When the rule measures a column | The column, for `freshness` and `missing_data_validation`. |
| `rule.params` | When the rule has parameters | The parameters the rule was evaluated with. |
| `outcome` | Always | `failed`, `warned`, `errored`, or `passed`. See [Rule outcomes](#rule-outcomes). |
| `status` | Always | The raw run status: `passed`, `failed`, or `error`. |
| `message` | Problems only | The human-readable result, the same text a Slack or email alert shows. |
| `observed` | Problems, when measured | The measurement: the age of the newest record for `freshness`, the number of short periods for `missing_data_validation`, the number of orphan rows for `async_foreign_key`. |
| `errorCodes` | Problems only | The `DI_E_` or `DI_W_` codes of the result. See [Results](object-level-validations.md#results). |
| `details` | Problems, when available | Rule-specific evidence: `last_seen` and `threshold` for freshness; `expected_min`, `slice`, `window`, and `missing_periods` for missing data; `constraints` and `totalOrphanRows` for foreign keys. Lists are sampled, see [Size limits](#size-limits). |
| `detailsOmitted` | When `details` was dropped for size | `true` when a rule's details exceeded 2 KB and were left out. |

Passing results carry only `rule`, `outcome`, and `status`, so a receiver sees what was checked without the evidence a problem needs.

## Rule outcomes

A result's outcome is its status read through its severity. The outcome names, prefixed `ai.peak.data_validation.rule.`, are the values a channel's `events` list accepts.

| `status` | `severity` | Outcome | Subscription value |
|----------|------------|---------|--------------------|
| `failed` | `error` | `failed` | `ai.peak.data_validation.rule.failed` |
| `failed` | `warn` | `warned` | `ai.peak.data_validation.rule.warned` |
| `error` | any | `errored` | `ai.peak.data_validation.rule.errored` |
| `passed` | any | `passed` | `ai.peak.data_validation.rule.passed` |
| `skipped` | any | `skipped` | None. Skipped rules are never delivered. |

A channel's event carries the rules that name the channel and whose outcome is in its `events` list, which defaults to all four for a webhook. A run in which none of the carried rules has a listed outcome produces no event. A run whose carried rules all passed has `outcome: passed` and reaches only channels subscribed to `ai.peak.data_validation.rule.passed`.

## Size limits

| Limit | Behavior |
|-------|----------|
| Results per event | At most 100. Beyond that, `results` holds the first 100 in outcome order, `resultsTotal` has the full count, and `resultsTruncated` is `true`. |
| Lists inside `details` (`missing_periods`, `missing_dates`) | Longer than 20 items, the last 20 are kept, with `<key>_total` and `<key>_truncated: true` alongside. |
| `details` per result | Larger than 2 KB after sampling, the details are replaced by `detailsOmitted: true`. |
| Whole body | About 60 KB. If a body is larger, `details` is dropped from every result; if it is still too large, `results` is halved repeatedly, down to none. `summary` and `resultsTotal` stay complete. |

## Delivery and retries

An event is handed to Peak's delivery service when the run completes. At that point the run report lists the channel in `alerts_sent`, which records that the event was accepted for delivery, not that your endpoint received it.

| Aspect | Behavior |
|--------|----------|
| Request | `POST`, with the body serialised once. The endpoint must respond within 10 seconds (5 seconds for the verification test event). The response body is ignored. |
| Success | Any `2xx` status. |
| Redirects | Never followed. A `3xx` is a permanent failure. |
| Retried | `408`, `425`, `429`, any `5xx`, a timeout, and connection or TLS failures. Six attempts in total, the retries after 10 seconds, 1 minute, 5 minutes, 30 minutes, and 2 hours. A `Retry-After` header on a `429` or `5xx` response lengthens the wait, within 5 seconds to 1 hour. After the sixth failed attempt the event is dropped. |
| Not retried | `3xx`, `410`, `401` or `403` when a credential was sent, any other `4xx`, a hostname that does not resolve, a URL that fails the [endpoint requirements](#endpoint-requirements), and a credential that is missing, expired, or in the wrong format. Fix the cause; the next run delivers normally. |
| Retries | Carry the same `webhook-id` and body, with a new `webhook-timestamp` and signature. Delivery is at-least-once, so deduplicate on `webhook-id`. |
| Order | Not guaranteed. A retried older event can arrive after a newer one; `time` and `data.run.id` tell them apart. |
| Source addresses | Deliveries originate from the Peak platform. If your endpoint restricts callers by IP address, contact support for the addresses to allow. |

## Endpoint requirements

- `https`, on port 443. An explicit `:443` is accepted.
- A public, fully qualified hostname. IP addresses, `localhost`, single-label names, and names ending in `.local`, `.localhost`, `.internal`, `.svc`, or `.cluster.local` are rejected when the channel is registered.
- Every address the hostname resolves to must be public. A name that resolves to a private, loopback, or link-local address is refused at send time with the reason `egress_blocked`, and the check is repeated on every delivery.
- TLS 1.2 or later, with a certificate issued by a public certificate authority. A self-signed certificate fails as a `network` error.
- No embedded credentials and no fragment in the URL. A query string is allowed.
- A `2xx` response within the timeout. Accept the event, then process it; a slow handler is retried as a `timeout`, and the retries repeat the same event.

## Verification test event

Registering, updating, or verifying a channel sends one `ai.peak.webhook.ping` event to the URL, with the same headers and signature as a delivery and `x-peak-event-type: ai.peak.webhook.ping`. The channel is stored only if the endpoint answers `2xx` within 5 seconds.

```json
{
  "specversion": "1.0",
  "id": "evt_7c1b9e7a-5d2f-4a1e-9b3c-2f6d8e0a4c11",
  "type": "ai.peak.webhook.ping",
  "source": "/peak/prod/notifications/acme",
  "subject": "alerts.example.com",
  "time": "2026-10-05T09:11:58.410Z",
  "datacontenttype": "application/json",
  "data": {
    "message": "Test event sent to verify this webhook destination. No action is required.",
    "tenant": { "id": "1234", "name": "acme" }
  }
}
```

## Compatibility

Fields are only ever added within an event type, so a receiver must ignore fields and enumeration values it does not recognise. `id`, `type`, `source`, `subject`, `data.run.id`, `data.outcome`, and `data.summary` keep their meaning. A breaking change ships as a new `type`.
