Channels
Deliver notifications to Slack, Teams, PagerDuty, or your own webhook.
A channel endpoint is a delivery target you configure — a Slack incoming-webhook URL, a Teams workflow URL, a PagerDuty routing key, or any HTTPS URL of your own. When an event fires with that channel enabled in your preferences, the notification fans out to every enabled endpoint of the matching type.
type | config | Notes |
|---|---|---|
slack | { "url": … } | Must be a hooks.slack.com incoming-webhook URL. Delivered as a Block Kit message with a severity color. |
teams | { "url": … } | A Teams Workflows (or legacy connector) webhook URL. Delivered as an Adaptive Card. |
pagerduty | { "routing_key": … } | An Events API v2 integration key. Retries of the same event collapse into one incident. |
webhook | { "url": …, "secret": … } | Any public HTTPS URL. secret is optional and enables request signing. |
in_app, email, and websocket are built in — they need no endpoint and
don't appear here. You can create up to 20 endpoints per account.
List endpoints
curl "$API_BASE/v1/notifications/channels" \
-H "Authorization: Bearer $TOKEN"from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.notifications.get_notifications_channels()
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.notifications.getNotificationsChannels();
{
"data": {
"items": [
{
"channel_id": "nch_9A4F72C1E08B4D3AA65201",
"type": "slack",
"label": "Data team Slack",
"enabled": true,
"config_hint": "hooks.slack.com (…XwZq)",
"has_secret": false,
"created_at": "2026-07-01T14:22:10+00:00",
"updated_at": "2026-07-01T14:22:10+00:00",
"last_result": { "at": "2026-07-14T18:05:44+00:00", "ok": true, "detail": "HTTP 200" }
}
]
}
}Full URLs and secrets never come back — config_hint is a masked
recognizer, and last_result reflects the most recent delivery attempt so
you can spot a broken endpoint at a glance.
Create an endpoint
curl -X POST "$API_BASE/v1/notifications/channels" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "webhook",
"label": "Ops receiver",
"config": {
"url": "https://ops.example.com/hooks/predictai",
"secret": "whsec_k3H9…"
}
}'from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.notifications.post_notifications_channels(json={
"type": "webhook",
"label": "Ops receiver",
"config": {
"url": "https://ops.example.com/hooks/predictai",
"secret": "whsec_k3H9…",
},
})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.notifications.postNotificationsChannels({
json: {
type: "webhook",
label: "Ops receiver",
config: {
url: "https://ops.example.com/hooks/predictai",
secret: "whsec_k3H9…",
},
},
});
{
"data": {
"channel": {
"channel_id": "nch_2D81B6F09C3E4A57B41830",
"type": "webhook",
"label": "Ops receiver",
"enabled": true,
"config_hint": "ops.example.com (…ctai)",
"has_secret": true,
"created_at": "2026-07-15T10:02:19+00:00",
"updated_at": "2026-07-15T10:02:19+00:00",
"last_result": null
}
}
}Request body
| Field | Meaning |
|---|---|
type required | slack, teams, pagerduty, or webhook. |
config required | The type's config from the table above — a url, or routing_key for PagerDuty, plus an optional secret for webhooks. |
label optional | Display name. |
enabled optional | Pass false to create the endpoint paused. Defaults to true. |
Errors
| Status | Why | Example message |
|---|---|---|
400 | Unknown type | "type must be one of ['pagerduty', 'slack', 'teams', 'webhook']" |
400 | URL missing, not HTTPS, or not a public host | "Webhook URL must use https" |
400 | Slack URL isn't an incoming webhook | "Slack URL must be a hooks.slack.com incoming-webhook URL" |
400 | Routing key too short to be real | "PagerDuty integration (routing) key looks invalid" |
400 | Already at 20 endpoints | "Endpoint limit reached (20)" |
Update an endpoint
Send only what you're changing:
| Field | Meaning |
|---|---|
label optional | Rename the endpoint. |
enabled optional | Pause (false) or resume (true) delivery. |
config optional | Partial too — omit url or secret and the stored value is kept. |
curl -X PUT "$API_BASE/v1/notifications/channels/nch_2D81B6F09C3E4A57B41830" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.notifications.put_notifications_channels_by_channel_id("nch_2D81B6F09C3E4A57B41830", json={"enabled": False})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.notifications.putNotificationsChannelsByChannelId("nch_2D81B6F09C3E4A57B41830", { json: { enabled: false } });
Returns 200 with the updated channel object. Config updates are
partial too: omit url or secret and the stored value is kept (you
never received the full value back, so you can't be expected to resend
it). Changing the config clears last_result — health is unknown until
the next delivery or test. An unknown channel_id
returns 404 with "Endpoint not found".
Delete an endpoint
curl -X DELETE "$API_BASE/v1/notifications/channels/nch_2D81B6F09C3E4A57B41830" \
-H "Authorization: Bearer $TOKEN"from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.notifications.delete_notifications_channels_by_channel_id("nch_2D81B6F09C3E4A57B41830")
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.notifications.deleteNotificationsChannelsByChannelId("nch_2D81B6F09C3E4A57B41830");
{ "data": { "deleted": "nch_2D81B6F09C3E4A57B41830" } }Deleting an endpoint doesn't touch your preferences — events that route to that channel type simply have one fewer place to go.
Test an endpoint
Synchronously pushes a canned message — titled "Test notification from Predict.ai" — through the endpoint and reports the result:
curl -X POST "$API_BASE/v1/notifications/channels/nch_9A4F72C1E08B4D3AA65201/test" \
-H "Authorization: Bearer $TOKEN"from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.notifications.post_notifications_channels_by_channel_id_test("nch_9A4F72C1E08B4D3AA65201")
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.notifications.postNotificationsChannelsByChannelIdTest("nch_9A4F72C1E08B4D3AA65201");
{ "data": { "ok": true, "detail": "HTTP 200" } }If the delivery fails, the response is 502 with the reason — e.g.
{ "data": { "ok": false, "detail": "HTTP 404: no service" } } or
"timed out". Either way, the attempt is stamped onto the endpoint's
last_result. Deliveries time out after 6 seconds.
Webhook payloads
Generic webhook endpoints receive a JSON POST per notification:
{
"delivery_id": "dlv_3f9c02ab714e58d0",
"event_code": "deployment.failed",
"category": "deployments",
"severity": "critical",
"title": "Deployment failed",
"body": "Daily sales forecaster failed to deploy: capacity check failed.",
"link": "https://app.predict.ai/workspaces/8b1f3c2a-…/deployments/c47d…",
"metadata": { "deployment_id": "c47d…", "workspace_id": "8b1f3c2a-…" },
"sent_at": "2026-07-15T10:11:38+00:00"
}Each request carries identifying headers:
| Header | Value |
|---|---|
X-PredictAI-Event | The event_code, e.g. deployment.failed |
X-PredictAI-Delivery | Unique per delivery, e.g. dlv_3f9c02ab714e58d0 |
X-PredictAI-Signature | sha256=<hex> — only when the endpoint has a secret |
User-Agent | PredictAI-Notifications/1.0 |
To verify a signed request, compute HMAC-SHA256 of the raw request body with your secret and compare against the header:
import hashlib, hmac
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
valid = hmac.compare_digest(expected, request.headers["X-PredictAI-Signature"])Respond with any 2xx within 6 seconds. Delivery is fire-and-forget —
a failed attempt is recorded on last_result but not retried, so keep
your receiver fast and treat the feed as
the source of truth.

