- Overview
- API Resources
Data quality alerts
Register Slack and email alert channels, then attach them to object-level validations so a failing check notifies your team.
Data quality alerts notify your team when an object-level validation fails, without anyone having to watch the Data Quality Dashboard. You register alert channels once for your tenant, then name them on the validations that should trigger them.
Slack and email channels are notified when a check finishes in a failed or error state; a check that passes sends nothing to them. A webhook channel delivers one structured event per run to an endpoint you operate, and can be told about passing checks too. This page covers Slack and email, and how any channel is attached to a validation. Webhook channels are covered in Webhook alerts.
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. - For a Slack channel, you have an incoming webhook URL. Only
httpsURLs onhooks.slack.comare accepted. - For an email channel, the recipient domain is allowed for your tenant. Alerts cannot be sent to an arbitrary address, so contact support to have a domain added before you register the channel.
- For a webhook channel, see the prerequisites in Webhook alerts.
Channel types
| Type | params | Notes |
|---|---|---|
email | email_addresses, an array of one or more addresses | Every address receives the same message. Each address must belong to a domain allowed for your tenant. |
slack | webhook, an incoming webhook URL | Registration posts a test message to the webhook, so a working URL confirms itself immediately. |
webhook | url, auth, and optionally headers and events | One signed JSON event per validation run, sent to an endpoint you operate. Registration sends a test event. See Webhook alerts. |
Register a channel
Each channel has a name that you use later on your validations. Names are unique within a tenant.
curl -X POST "https://ingestion.peak.ai/api/v2/alert-configs" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "email",
"name": "data-team",
"params": { "email_addresses": ["data-team@example.com"] }
}'
curl -X POST "https://ingestion.peak.ai/api/v2/alert-configs" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "email",
"name": "data-team",
"params": { "email_addresses": ["data-team@example.com"] }
}'
A Slack channel is registered the same way, with a webhook instead of addresses:
curl -X POST "https://ingestion.peak.ai/api/v2/alert-configs" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "slack",
"name": "data-alerts-channel",
"params": { "webhook": "https://hooks.slack.com/services/T000/B000/XXXX" }
}'
curl -X POST "https://ingestion.peak.ai/api/v2/alert-configs" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "slack",
"name": "data-alerts-channel",
"params": { "webhook": "https://hooks.slack.com/services/T000/B000/XXXX" }
}'
A successful request returns 201 Created with the stored channel and "verified": true:
{
"type": "slack",
"name": "data-alerts-channel",
"params": { "webhook": "https://hooks.slack.com/services/T000/B000/XXXX" },
"verified": true,
"verifiedAt": 1787136239267
}
{
"type": "slack",
"name": "data-alerts-channel",
"params": { "webhook": "https://hooks.slack.com/services/T000/B000/XXXX" },
"verified": true,
"verifiedAt": 1787136239267
}
Verification is part of registering a Slack channel. If the webhook does not return a success response, the channel is rejected with 400 Bad Request and nothing is stored, so you find out about a wrong URL straight away.
A webhook channel is registered through the same endpoint. Its params and the test event sent on registration are described in Register a webhook channel.
Allowed recipient domains
Email recipients are checked against a list of domains allowed for your tenant. An address outside that list is rejected with 400 Bad Request and the channel is not stored, so a customer domain has to be allowed before alerts can reach it. A domain can be allowed in full, or narrowed to specific addresses within it. Contact support to add one.
Alert emails also count towards your tenant's daily email quota, alongside any other email the platform sends on your behalf. A validation that keeps failing sends a message on every run, so a persistently failing check contributes to that total.
Choose which outcomes notify a 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). A Slack or email channel is notified for failed, warned, and errored unless its params carry an events array naming the outcomes to deliver, each prefixed ai.peak.data_validation.rule.:
{
"type": "email",
"name": "data-team",
"params": {
"email_addresses": ["data-team@example.com"],
"events": ["ai.peak.data_validation.rule.errored"]
}
}
{
"type": "email",
"name": "data-team",
"params": {
"email_addresses": ["data-team@example.com"],
"events": ["ai.peak.data_validation.rule.errored"]
}
}
This channel hears only about checks that could not run; failures and warnings are not delivered to it. The list must not be empty or repeat a value. ai.peak.data_validation.rule.passed is accepted but has no effect on Slack and email, which carry problems only, so a list containing nothing else silences the channel. GET returns events as stored, and a channel registered without it keeps the default.
A webhook channel uses the same list to choose what its event carries, with passing checks included by default. See Choose which outcomes notify the channel.
Manage channels
| Method | Path | Purpose |
|---|---|---|
GET | /api/v2/alert-configs | List every channel registered for the tenant. |
GET | /api/v2/alert-configs/{name} | Fetch one channel. |
PUT | /api/v2/alert-configs/{name} | Replace a channel's type and params, then re-run verification. |
DELETE | /api/v2/alert-configs/{name} | Delete a channel. |
POST | /api/v2/alert-configs/{name}/verify | Re-run verification using the stored params. |
Updating a channel replaces its type and params, so send the full set of recipients rather than only the ones you are adding:
curl -X PUT "https://ingestion.peak.ai/api/v2/alert-configs/data-team" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "email",
"params": { "email_addresses": ["data-team@acme.com", "oncall@acme.com"] }
}'
curl -X PUT "https://ingestion.peak.ai/api/v2/alert-configs/data-team" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "email",
"params": { "email_addresses": ["data-team@acme.com", "oncall@acme.com"] }
}'
The channel is identified by the path, so a name in an update body is optional and cannot differ from it. To rename a channel, register a new one and repoint your validations.
An update re-runs verification. Changing a Slack channel's webhook, or a webhook channel's destination, to one that does not accept the test message is rejected with 400 Bad Request and the stored channel is left as it was.
Use verify to confirm a Slack webhook or a webhook endpoint still works, for example after someone changes the app in Slack or rotates a secret. A destination that no longer accepts the test message returns 400 Bad Request.
Deleting a channel that a validation still names returns 409 Conflict, and the response identifies the table holding the reference. Remove the name from the validation first.
Attach channels to a validation
Add an alerts array to any freshness, missing_data_validation, or async_foreign_key entry in objectValidations, listing the channel names to notify. Set it when you save a schema or update it later with Patch a solution's schema.
"objectValidations": [
{ "type": "freshness",
"params": { "column": "order_date", "threshold": 24, "unit": "hours" },
"enabled": true, "severity": "error",
"alerts": ["data-team", "data-alerts-channel"] },
{ "type": "missing_data_validation",
"params": { "column": "order_date", "interval": 1, "unit": "days", "minRowCount": 100 },
"enabled": true, "severity": "warn",
"alerts": ["__ALL__"] }
]
"objectValidations": [
{ "type": "freshness",
"params": { "column": "order_date", "threshold": 24, "unit": "hours" },
"enabled": true, "severity": "error",
"alerts": ["data-team", "data-alerts-channel"] },
{ "type": "missing_data_validation",
"params": { "column": "order_date", "interval": 1, "unit": "days", "minRowCount": 100 },
"enabled": true, "severity": "warn",
"alerts": ["__ALL__"] }
]
To add alerts to a table that is already live, send the entry through Patch a solution's schema:
curl -X PATCH "https://ingestion.peak.ai/api/v2/schema/solutions" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"solutionName": "orders-pipeline",
"tables": [
{
"objectName": "orders",
"objectValidations": [
{ "type": "freshness",
"params": { "column": "order_date", "threshold": 24, "unit": "hours" },
"enabled": true, "severity": "error",
"alerts": ["data-team"] }
]
}
]
}'
curl -X PATCH "https://ingestion.peak.ai/api/v2/schema/solutions" \
-H "Authorization: $PEAK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"solutionName": "orders-pipeline",
"tables": [
{
"objectName": "orders",
"objectValidations": [
{ "type": "freshness",
"params": { "column": "order_date", "threshold": 24, "unit": "hours" },
"enabled": true, "severity": "error",
"alerts": ["data-team"] }
]
}
]
}'
objectValidations replaces the table's existing list rather than merging into it, so include every rule you want to keep, not only the one you are changing.
__ALL__ is a reserved name that expands to every channel registered for the tenant, webhook channels included, so a new channel starts receiving those alerts without editing any schema. Mixing __ALL__ with an explicit name does not duplicate the message.
A name that does not match a registered channel is rejected with 400 Bad Request when you save the validation, so a typo surfaces immediately.
What an alert contains
Every alert names where the failure happened, which check failed, and what that check required.
Slack and email present those facts differently. Slack sends a message for each failing check, whichever table of the run it is on. Email gathers a whole run into one message, as described in One email per person per run. A webhook channel receives the structured event described in the Webhook event reference, grouped by table, instead of a rendered message.
| Field | Description |
|---|---|
| Tenant, Solution, Table | Where the failure happened. |
| Check | The rule that failed and what it ran on, for example Freshness (order_date), Missing data (order_date), or Foreign key (fk_items_order). |
| Alert level | Error or Warning, from the rule's severity. |
| Error code | The code for this failure, for example DI_E_24F01. Use it to find the same failure in the Data Quality Dashboard. |
| Measurement | The observed value, labelled by rule: the age of the newest record, how many periods fell short, or how many rows reference a missing parent. |
| Validation criteria | What the rule required, in words, for example Data must be no more than 24 hours old. |
| Triggered by | The caller who started the run, or Peak platform for a run with no caller. |
| Detected at | When the failure was recorded, in UTC. |
| Run ID | The validation run, for cross-referencing with the dashboard. |
One email per person per run
A validation run checks one or more tables of a solution, and a single run can fail several checks at once. Email gathers those failures into one message for each person.
Someone named on two channels therefore hears about a run once, rather than once per channel. The email carries every failure that the channels they are named on would have sent them, from every table of the run.
Inside the email:
- Failures are grouped by check, with a row for each column or constraint that failed. When the run spans several tables, each group is one table and one check, such as
stage.orders: Freshness, and the header lists the tables, or counts them when the list is long. - A person with more than 10 failures in a run receives a summary instead of the full list. It has one row per check type, giving the range of measurements, the limit, the three worst places as
table.column, and how many more failed; a row for checks that could not run; the five most affected tables with their error and warning counts; and a link to the dashboard. - Slack works the other way. Each failing check sends its own message, whichever table it is on, so a channel shows one line per failure.
Example alert
Take a run on stage.order_items that fails three checks. The email arrives with the subject [ERROR] 3 checks failed: stage.order_items, and names the run once. A run over several tables puts the solution in the subject and counts the tables, for example [ERROR] 7 checks failed across 2 tables: orders-pipeline.
| Tenant | acme |
| Solution | orders-pipeline |
| Triggered by | dana@acme.com |
| Detected at | 31 Aug 2026, 09:12 UTC |
It then groups the three failures by check:
| Check | Failed on | Measurement | Validation criteria |
|---|---|---|---|
| Freshness, 1 error | order_date (DI_E_24F01) | 50 hours since last record | Data must be no more than 24 hours old |
| Missing data, 1 warning | order_date (DI_W_24M01) | 3 of 24 hourly periods short | Minimum 100 rows every day |
| Foreign key, 1 error | fk_items_order (DI_E_23F01) | 1,432 orphan rows | Every row must match a row in stage.orders |
Slack sends one message per failing check, led by the table name so it is readable in a busy channel:
❌ Data validation failed: stage.order_items
Data is stale: column 'order_date' is 50 hours old (threshold: 24 hours)
Tenant acme Solution orders-pipeline
Table stage.order_items Check Freshness (order_date)
Alert level Error · DI_E_24F01 Latest record age 50 hours
Triggered by dana@acme.com Detected at 31 Aug 2026, 09:12 UTC
Validation criteria
Data must be no more than 24 hours old
Run e3f1c2a4-9b77-4d21-8c5e-1a2b3c4d5e6f
❌ Data validation failed: stage.order_items
Data is stale: column 'order_date' is 50 hours old (threshold: 24 hours)
Tenant acme Solution orders-pipeline
Table stage.order_items Check Freshness (order_date)
Alert level Error · DI_E_24F01 Latest record age 50 hours
Triggered by dana@acme.com Detected at 31 Aug 2026, 09:12 UTC
Validation criteria
Data must be no more than 24 hours old
Run e3f1c2a4-9b77-4d21-8c5e-1a2b3c4d5e6f
A missing_data_validation alert reads the same way, with a count of periods in place of the record age. An async_foreign_key alert names the constraint that failed and reports how many rows reference a missing parent.
Behavior to expect
- Email sends one message per person per run, covering every failure they should see on every table the run covered. Slack sends one message per failing check per channel. A webhook channel receives one event per run, listing every check it carries, grouped by table.
- A check that keeps failing alerts again on every run, until it passes.
- The run report lists the channels notified for each result in
alerts_sent, which is useful for confirming that a new channel is wired up. For a webhook channel,alerts_sentrecords that the event was accepted for delivery; whether your endpoint received it is visible only at the endpoint. - A channel that cannot be reached does not fail the validation run. The run completes, and the channel is left out of
alerts_sent.