- Overview
- API Resources
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.
An alert is sent when a check finishes in a failed or error state. A check that passes sends nothing.
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.
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. |
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.
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.
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 to one that does not accept messages is rejected with 400 Bad Request and the stored channel is left as it was.
Use verify to confirm a Slack webhook still works, for example after someone changes the app in Slack. A webhook that no longer accepts messages 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 or missing_data_validation 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, 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
Both channels carry the same facts, so an alert reads the same in a mailbox or a Slack channel:
| Field | Description |
|---|---|
| Tenant, Solution, Table | Where the failure happened. |
| Check | The rule that failed, for example Freshness or Missing data. |
| Alert level | Error or Warning, from the rule's severity. |
| Measurement | The observed value, labelled by rule. Freshness reports the age of the newest record; missing data reports how many periods fell short. |
| Validation criteria | What the rule required, in words, for example Data must be no more than 24 hours old (order_date). |
| 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. |
Example alert
For a freshness check on stage.orders that requires data no older than 24 hours, where the newest record is 50 hours old, the email arrives with the subject [ERROR] Data validation failed: stage.orders and these details:
| Tenant | acme |
| Solution | orders-pipeline |
| Table | stage.orders |
| Check | Freshness |
| Alert level | Error |
| Latest record age | 50 hours |
| Validation criteria | Data must be no more than 24 hours old (order_date) |
| Triggered by | dana@acme.com |
| Detected at | 31 Aug 2026, 09:12 UTC |
The Slack message carries the same details, led by the table name so it is readable in a busy channel:
❌ Data validation failed: stage.orders
Data is stale: column 'order_date' is 50 hours old (threshold: 24 hours)
Tenant acme Solution orders-pipeline
Table stage.orders Check Freshness
Alert level Error 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 (order_date)
Run e3f1c2a4-9b77-4d21-8c5e-1a2b3c4d5e6f
❌ Data validation failed: stage.orders
Data is stale: column 'order_date' is 50 hours old (threshold: 24 hours)
Tenant acme Solution orders-pipeline
Table stage.orders Check Freshness
Alert level Error 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 (order_date)
Run e3f1c2a4-9b77-4d21-8c5e-1a2b3c4d5e6f
A missing_data_validation alert reads the same way, with Missing data as the check and a count of periods in place of the record age, for example 3 of 24 hourly periods.
Behavior to expect
- One message is sent per failing check per channel. Two failing checks with two channels produce four messages from a single run.
- 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. - A channel that cannot be reached does not fail the validation run. The run completes, and the channel is left out of
alerts_sent.