Notification destinations define where STACK sends alerts. Channel rules map events to those destinations. Verify a destination before you add it to a rule.
Notification configuration is restricted to account administrators. Agent and Passport credentials cannot read or change it. A dashboard administrator can make changes directly; an API or OAuth request must be approved in the dashboard and retried with the returnedX-Governance-Approval header.
A destination is a verified address where STACK can send notifications. Three channel types are supported: email, sms, and webhook. Destinations must be verified before they can receive notifications. Email is available on every tier; SMS and webhook destinations require a paid tier and return 403 on free.
POST /v1/notifications/destinations
{
"channel_type": "email",
"destination": "alerts@example.com"
}# Email destination
curl -X POST https://api.getstack.run/v1/notifications/destinations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel_type": "email",
"destination": "alerts@example.com"
}'
# Webhook destination
curl -X POST https://api.getstack.run/v1/notifications/destinations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel_type": "webhook",
"destination": "https://example.com/hooks/stack"
}'
# SMS destination
curl -X POST https://api.getstack.run/v1/notifications/destinations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel_type": "sms",
"destination": "+14155551234"
}'{
"id": "ndst_abc123",
"operator_id": "op_abc123",
"channel_type": "email",
"destination": "alerts@example.com",
"verified": false,
"created_at": "2026-04-15T10:00:00.000Z"
}Email destinations are auto-verified if the email matches the operator's registered email. Every other destination, webhook included, requires the two-step verification flow. Creating a webhook returns its webhook_secret once; store it before leaving the response. STACK then POSTs the verification code to the URL, signed with that secret. Read the code from your endpoint and submit it back. Nothing is delivered to an unverified destination.
{
"id": "ndst_webhook123",
"operator_id": "op_abc123",
"channel_type": "webhook",
"destination": "https://example.com/hooks/stack",
"verified": false,
"webhook_secret": "whsec_...",
"created_at": "2026-04-15T10:00:00.000Z"
}GET /v1/notifications/destinations
curl https://api.getstack.run/v1/notifications/destinations \
-H "Authorization: Bearer YOUR_API_KEY"DELETE /v1/notifications/destinations/:id
curl -X DELETE https://api.getstack.run/v1/notifications/destinations/ndst_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"{
"deleted": true
}POST /v1/notifications/destinations/:id/rotate-secret
Replaces a webhook destination's signing secret. The replacement is returned once in the response and is never included when destinations or channel rules are listed.
curl -X POST https://api.getstack.run/v1/notifications/destinations/ndst_webhook123/rotate-secret \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Governance-Approval: gvr_abc123"Verification uses a code. Email and SMS receive the code directly. A webhook receives a signed request containing the code. Submit that code to complete verification.
POST /v1/notifications/destinations/:id/send-code
curl -X POST https://api.getstack.run/v1/notifications/destinations/ndst_abc123/send-code \
-H "Authorization: Bearer YOUR_API_KEY"POST /v1/notifications/destinations/:id/verify
curl -X POST https://api.getstack.run/v1/notifications/destinations/ndst_abc123/verify \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "123456"
}'{
"verified": true
}If the code is invalid or expired, the response includes an error message.
Destinations must be verified before they can be used in notification channel rules. Creating a rule with unverified destinations returns a 400 error.
POST /v1/notifications/destinations/:id/test
Send a test notification to a verified destination to confirm it works correctly.
curl -X POST https://api.getstack.run/v1/notifications/destinations/ndst_abc123/test \
-H "Authorization: Bearer YOUR_API_KEY"Channels are rules that map event types to one or more notification destinations. When a matching event fires, STACK delivers a notification to all destinations on the rule.
POST /v1/notifications/channels
{
"destination_ids": ["ndst_abc123", "ndst_def456"],
"events": ["passport.flagged", "agent.blocked"],
"min_severity": "warning"
}curl -X POST https://api.getstack.run/v1/notifications/channels \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_ids": ["ndst_abc123"],
"events": ["passport.flagged", "passport.blocked", "agent.blocked"],
"min_severity": "warning"
}'{
"id": "nch_abc123",
"operator_id": "op_abc123",
"channel_type": "email",
"destination": "alerts@example.com",
"destination_id": "ndst_abc123",
"destination_ids": ["ndst_abc123"],
"events": ["passport.flagged", "passport.blocked", "agent.blocked"],
"min_severity": "warning",
"verified": true,
"active": true,
"created_at": "2026-04-15T10:10:00.000Z"
}GET /v1/notifications/channels
Returns all channel rules with enriched delivery_methods array containing the full destination objects.
curl https://api.getstack.run/v1/notifications/channels \
-H "Authorization: Bearer YOUR_API_KEY"PATCH /v1/notifications/channels/:id
{
"destination_ids": ["ndst_abc123", "ndst_ghi789"],
"events": ["passport.flagged", "passport.blocked", "agent.blocked", "passport.missed_checkout"],
"min_severity": "info"
}curl -X PATCH https://api.getstack.run/v1/notifications/channels/nch_abc123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": ["passport.flagged", "agent.blocked"],
"min_severity": "critical"
}'DELETE /v1/notifications/channels/:id
curl -X DELETE https://api.getstack.run/v1/notifications/channels/nch_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"{
"deleted": true
}These are the event types you can use in notification channel rules:
When creating rules without specifying events, the system defaults to all four event types: passport.flagged, passport.blocked, agent.blocked, and passport.missed_checkout.
When a rule fires and delivers to a webhook destination, STACK sends a POST request. Webhook destinations receive a webhook_secret on creation that can be used to verify the payload signature.
{
"event_type": "passport.flagged",
"severity": "warning",
"payload": {
"jti": "ppt_abc123",
"agent_id": "agt_xyz",
"flags": [
{
"type": "undeclared_service",
"severity": "warning",
"message": "Agent accessed slack but did not declare it in intent"
}
]
},
"timestamp": "2026-04-15T10:05:00.000Z"
}Every webhook request carries an X-Stack-Signatureheader. The value is the HMAC-SHA256 of the raw request body, keyed with thewebhook_secret returned when the destination was created, encoded as lowercase hex.
X-Stack-Signature: a7f9…c2e1
signature = hex( HMAC_SHA256(secret = webhook_secret, message = raw_request_body) )Verify before acting on the payload. Use a constant-time comparison to avoid timing attacks, and hash the raw body (not a re-serialized JSON) so the bytes match what STACK signed.
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.STACK_WEBHOOK_SECRET;
const app = express();
// Capture the raw body for HMAC verification.
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/stack', (req, res) => {
const sig = req.header('x-stack-signature');
const computed = crypto
.createHmac('sha256', SECRET)
.update(req.body) // req.body is a Buffer here
.digest('hex');
const ok =
sig &&
sig.length === computed.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(computed));
if (!ok) return res.status(401).send('invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
// …handle event…
res.status(204).end();
});import hmac, hashlib, os
from flask import Flask, request, abort
SECRET = os.environ["STACK_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/webhooks/stack")
def stack_webhook():
sig = request.headers.get("X-Stack-Signature", "")
computed = hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, computed):
abort(401)
event = request.get_json()
# …handle event…
return "", 204Webhook delivery is at most once. Return a 2xx response only after you store the event. STACK does not retry a failed or timed-out delivery.