- Overview
- API Resources
Webhook alerts
Register a webhook alert channel for object-level validations, store its secret as an external credential, verify signed deliveries, and rotate the secret.
A webhook alert channel sends the outcome of each object-level validation run to an HTTPS endpoint you operate, as a signed JSON event. Use it to feed validation results into an incident tool, a workflow engine such as Power Automate, or a service of your own. Slack and email channels are covered in Data quality alerts, and the event your endpoint receives is specified in the Webhook event reference.
A webhook channel receives one event per validation run, across every table the run covered. The event carries every rule in that run which names the channel and ends in an outcome the channel subscribes to, grouped by table. A channel can subscribe to passing rules too, so a clean run can notify it.
Before you start
- At least one table has been rolled out with object-level validations (see Object-level validations).
- You have a Personal Access Token (see Getting Started).
- You know whether your tenant is on the hub (
https://ingestion.peak.ai) or a spoke cluster (https://ingestion.<cluster-identifier>.peak.ai). Spoke tenants must use their spoke host. - Your endpoint accepts
POSTrequests at anhttpsURL on a public, fully qualified hostname, on port 443, and answers with a2xxstatus within 5 seconds. Redirects are not followed. See Endpoint requirements. - If the endpoint expects a signature, token, password, or API key, you can add external credentials in Peak Settings (see Add an external credential).
Store the secret as an external credential
The channel never holds a secret. Each secret-bearing field in its auth block names a Custom external credential, and the value is read from Peak at the moment an event is sent. Skip this section for no-auth.
-
Generate the value your endpoint will check deliveries against. For HMAC signing, the value is base64 text, optionally prefixed with
whsec_, that decodes to at least 16 bytes:echo "whsec_$(openssl rand -base64 32)"echo "whsec_$(openssl rand -base64 32)"For bearer, basic, or API-key authentication, the value is the token, password, or key your endpoint already expects.
-
Configure the same value on your endpoint.
-
In Peak, open Settings and select Access tokens.
-
In External credentials, select Add.
-
Enter a name, such as
peak_webhook_signing_secret. The name must start with a letter and may contain letters, digits,_, and-. -
Choose the Custom type.
-
Enter the value and select Submit.
The name is all that appears in the channel, in GET responses, and in logs.
Register a webhook channel
Register the channel with POST /api/v2/alert-configs, naming the credential in auth. Registration sends a signed test event (ai.peak.webhook.ping) to the URL and stores the channel only if the endpoint answers 2xx within 5 seconds.
curl -X POST "https://ingestion.peak.ai/api/v2/alert-configs" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "webhook",
"name": "oncall-hook",
"params": {
"url": "https://alerts.example.com/hooks/peak",
"headers": { "X-Env": "prod" },
"auth": { "type": "hmac", "secret": "peak_webhook_signing_secret" },
"events": [
"ai.peak.data_validation.rule.failed",
"ai.peak.data_validation.rule.errored"
]
}
}'
curl -X POST "https://ingestion.peak.ai/api/v2/alert-configs" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "webhook",
"name": "oncall-hook",
"params": {
"url": "https://alerts.example.com/hooks/peak",
"headers": { "X-Env": "prod" },
"auth": { "type": "hmac", "secret": "peak_webhook_signing_secret" },
"events": [
"ai.peak.data_validation.rule.failed",
"ai.peak.data_validation.rule.errored"
]
}
}'
A successful request returns 201 Created with the stored channel and "verified": true:
{
"type": "webhook",
"name": "oncall-hook",
"params": {
"url": "https://alerts.example.com/hooks/peak",
"headers": { "X-Env": "prod" },
"auth": { "type": "hmac", "secret": "peak_webhook_signing_secret" },
"events": [
"ai.peak.data_validation.rule.failed",
"ai.peak.data_validation.rule.errored"
]
},
"verified": true,
"verifiedAt": 1791191520000
}
{
"type": "webhook",
"name": "oncall-hook",
"params": {
"url": "https://alerts.example.com/hooks/peak",
"headers": { "X-Env": "prod" },
"auth": { "type": "hmac", "secret": "peak_webhook_signing_secret" },
"events": [
"ai.peak.data_validation.rule.failed",
"ai.peak.data_validation.rule.errored"
]
},
"verified": true,
"verifiedAt": 1791191520000
}
If the endpoint does not accept the test event, the request fails with 400 Bad Request and nothing is stored. The message names the reason, for example Webhook verification failed: http_4xx (HTTP 404). See Verification failures.
params accepts exactly four keys. Any other key is rejected.
| Key | Required | Rules |
|---|---|---|
url | Yes | https only, at most 2048 characters. The host must be a public, fully qualified hostname, not an IP address, with no embedded credentials and no fragment. The port must be 443 or omitted. A query string is allowed, so a Power Automate or Logic Apps URL that carries its signature in the query works as it is. |
auth | Yes | How deliveries authenticate to the endpoint. See Authentication methods. no-auth has to be chosen explicitly. |
headers | No | Up to 10 static, non-secret headers sent with every delivery. See Static headers. |
events | No | The rule outcomes that notify the channel. Defaults to all four. See Choose which outcomes notify the channel. |
Authentication methods
auth.type selects one method, and auth may contain only the fields of that method. Every field that would hold a secret holds the name of an external credential instead.
auth.type | Credential field | Other fields | What the endpoint receives |
|---|---|---|---|
hmac | secret | none | A webhook-signature header, an HMAC-SHA256 over the request body following the Standard Webhooks specification. Recommended: it identifies the sender and proves the body was not altered. |
bearer-token | bearerToken | none | Authorization: Bearer <token> |
basic | password | username, sent as it is; at most 256 characters, no : | Authorization: Basic <base64 of username:password> |
api-key | apiKey | headerName, default X-API-Key. Authorization is allowed; other reserved names are not. | The key, in the header named by headerName |
no-auth | none | none | No authentication header. The webhook-id and webhook-timestamp headers are still sent. |
The auth object takes one of these shapes, where <your-credential-name> is the name of the external credential that holds the value:
{ "type": "no-auth" }
{ "type": "hmac", "secret": "<your-credential-name>" }
{ "type": "bearer-token", "bearerToken": "<your-credential-name>" }
{ "type": "basic", "username": "peak-alerts", "password": "<your-credential-name>" }
{ "type": "api-key", "apiKey": "<your-credential-name>", "headerName": "X-API-Key" }
{ "type": "no-auth" }
{ "type": "hmac", "secret": "<your-credential-name>" }
{ "type": "bearer-token", "bearerToken": "<your-credential-name>" }
{ "type": "basic", "username": "peak-alerts", "password": "<your-credential-name>" }
{ "type": "api-key", "apiKey": "<your-credential-name>", "headerName": "X-API-Key" }
oauth is reserved for a later release and is rejected with 400 Bad Request.
Static headers
headers adds fixed headers to every delivery, for routing or environment tags. Each entry must satisfy these rules:
- The name is a valid HTTP header name of at most 64 characters, and names are unique ignoring case.
- The value is printable ASCII of 1 to 1024 characters, with no line breaks.
- Names the delivery service owns are rejected:
host,authorization,proxy-authorization,cookie,user-agent,accept-encoding,connection,keep-alive,transfer-encoding,te,trailer,upgrade,expect, and any name starting withcontent-,webhook-, orx-peak-. - Names that look like they carry a secret are rejected: any name starting with
author containingtoken,secret,password,apikey,api-key, orapi_key. Secrets belong inauth.
Choose which outcomes notify the channel
Every rule result has an outcome: failed (the check failed and the rule has severity: error), warned (the check failed and the rule has severity: warn), errored (the rule could not be evaluated, which includes every rule of a table that timed out or has an invalid configuration), or passed (the check held). events lists the outcomes that notify the channel, each prefixed ai.peak.data_validation.rule.:
"events": ["ai.peak.data_validation.rule.failed", "ai.peak.data_validation.rule.errored"]
"events": ["ai.peak.data_validation.rule.failed", "ai.peak.data_validation.rule.errored"]
- When
eventsis omitted, a webhook channel is notified for all four outcomes, so it also hears about clean runs. - The event carries only the rules whose outcome is listed, and its counts cover only those. A run with none of the listed outcomes sends nothing to the channel.
- To gate a pipeline on a run, keep all four outcomes. A channel that lists
ai.peak.data_validation.rule.passedwithout the problem outcomes receivesoutcome: passedfor a run whose other checks failed. - Entries must be distinct, and the list must not be empty.
ai.peak.data_validation.run.completedandai.peak.webhook.pingare event types rather than outcomes and are rejected. - Slack and email channels accept the same list with a narrower meaning. See Choose which outcomes notify a channel.
Attach the channel to validations
Add the channel's name to the alerts array of a freshness, missing_data_validation, or async_foreign_key entry, exactly as for a Slack or email channel. See Attach channels to a validation.
The reserved name __ALL__ expands to every channel registered for the tenant, webhook channels included. A validation that uses it starts posting to a new webhook channel as soon as the channel is registered, and with the default events that includes clean runs.
Verify the signature
With auth.type: hmac, every delivery carries webhook-id, webhook-timestamp, and webhook-signature headers. Verify them before trusting the body:
- Read the raw request body as bytes, before any JSON parsing. The signature covers these exact bytes, and a re-serialised copy will not match.
- Decode the signing key: strip the optional
whsec_prefix from the credential value and base64-decode the rest. - Compute HMAC-SHA256 over the string
<webhook-id>.<webhook-timestamp>.<body>with that key, and base64-encode the result. - Compare it in constant time with the value after
v1,inwebhook-signature. - Reject the request if
webhook-timestamp(Unix seconds) is more than five minutes from the current time. - Respond with a
2xxstatus, then process the event. Delivery is at-least-once, so keep thewebhook-idof events already processed and ignore repeats.
The verification test event sent on registration carries the same headers and signature, with x-peak-event-type: ai.peak.webhook.ping, so this code runs before the channel is stored.
The Standard Webhooks libraries implement steps 2 to 5. With the standardwebhooks package, a Flask endpoint looks like this:
import os
from flask import Flask, abort, request
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
# The credential value (whsec_ followed by base64), read from your own secret store.
webhook = Webhook(os.environ["PEAK_WEBHOOK_SECRET"])
@app.post("/hooks/peak")
def receive_peak_event():
try:
event = webhook.verify(request.get_data(), dict(request.headers))
except WebhookVerificationError:
abort(401)
# event is the parsed CloudEvent. Deduplicate on event["id"], then process it.
return "", 204
import os
from flask import Flask, abort, request
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
# The credential value (whsec_ followed by base64), read from your own secret store.
webhook = Webhook(os.environ["PEAK_WEBHOOK_SECRET"])
@app.post("/hooks/peak")
def receive_peak_event():
try:
event = webhook.verify(request.get_data(), dict(request.headers))
except WebhookVerificationError:
abort(401)
# event is the parsed CloudEvent. Deduplicate on event["id"], then process it.
return "", 204
The same endpoint in Express, with the npm package of the same name:
import express from "express";
import { Webhook, WebhookVerificationError } from "standardwebhooks";
const app = express();
// The credential value (whsec_ followed by base64), read from your own secret store.
const webhook = new Webhook(process.env.PEAK_WEBHOOK_SECRET);
app.post("/hooks/peak", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = webhook.verify(req.body, req.headers);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return res.sendStatus(401);
}
throw error;
}
// event is the parsed CloudEvent. Deduplicate on event.id, then process it.
res.sendStatus(204);
});
import express from "express";
import { Webhook, WebhookVerificationError } from "standardwebhooks";
const app = express();
// The credential value (whsec_ followed by base64), read from your own secret store.
const webhook = new Webhook(process.env.PEAK_WEBHOOK_SECRET);
app.post("/hooks/peak", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = webhook.verify(req.body, req.headers);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return res.sendStatus(401);
}
throw error;
}
// event is the parsed CloudEvent. Deduplicate on event.id, then process it.
res.sendStatus(204);
});
Rotate a secret
The value is looked up when an event is sent and kept for up to five minutes, so rotating a secret does not require changing the channel.
-
Make your endpoint accept both the current value and the new one.
-
In Peak, open Settings, select Access tokens, and edit the credential under External credentials with the new value.
-
After five minutes, send a fresh test event to confirm the new value works:
curl -X POST "https://ingestion.peak.ai/api/v2/alert-configs/oncall-hook/verify" \ -H "Authorization: $PEAK_AUTH_TOKEN"curl -X POST "https://ingestion.peak.ai/api/v2/alert-configs/oncall-hook/verify" \ -H "Authorization: $PEAK_AUTH_TOKEN" -
Remove the old value from your endpoint.
To move a channel to a different credential instead, update it with PUT /api/v2/alert-configs/{name}, sending the full type and params with the new name in auth. The update is verified like a registration, and the stored channel is left unchanged if the test event is refused.
Verification failures
Registration, update, and verify fail with 400 Bad Request and Webhook verification failed: <reason> (HTTP <status>) when the test event is not accepted. The HTTP status is included when the endpoint answered.
| Reason | Meaning | What to check |
|---|---|---|
http_4xx | The endpoint rejected the event. | The URL path, and that the endpoint accepts POST with a JSON body. |
invalid_credential | The endpoint answered 401 or 403 to the configured credential. | The credential value matches what the endpoint expects, and auth.type is the method the endpoint uses. |
http_3xx | The endpoint redirected. Redirects are never followed. | Register the URL the redirect points to. |
http_5xx | The endpoint returned a server error. | The endpoint's logs. |
timeout | No response within 5 seconds (10 seconds for a real delivery). | Respond before doing any processing. |
network | The connection or the TLS handshake failed. | The endpoint is reachable from the internet and presents a certificate issued by a public certificate authority. |
dns_nxdomain | The hostname does not resolve. | The hostname in url. |
egress_blocked | The hostname resolves to a private or internal address, or the URL is otherwise not allowed. | Endpoint requirements. |
endpoint_gone | The endpoint returned 410 Gone. | Register a current URL. |
throttled | The endpoint returned 429. | Retry, or raise the endpoint's limit. |
credential_missing | No external credential has the name given in auth. | The name, including its case. |
credential_expired | The credential has passed its expiry date. | Extend it or add a new credential. |
credential_invalid_format | For hmac, the value is not base64 or decodes to fewer than 16 bytes. | Generate the value as shown in Store the secret as an external credential. |
credential_unavailable, credential_lookup_rejected, unknown | Peak could not read the credential. | Retry, and contact support if it persists. |
Two other responses can occur. 429 Too Many Requests means the tenant has sent about ten test events within a minute; wait a minute and retry. 400 Bad Request with Webhook verification service is unavailable; retry shortly is a platform-side condition; retry.