Notifications API
The Notifications API lets you configure how and when STACK alerts you about events in your organization. The system has two concepts: destinations (where to send) and channels (rules that map events to destinations). Set up and verify destinations first, then create channel rules to route events.
All endpoints require an Authorization: Bearer sk_live_... header with a valid operator API key or team member API key.
Notification Destinations
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.
Add a Destination
POST /v1/notifications/destinations
{
"channel_type": "email",
"destination": "alerts@example.com"
}- channel_type (string, required) - One of "email", "sms", or "webhook".
- destination (string, required) - The email address, phone number, or webhook URL.
Request Examples
# Email destination
curl -X POST https://api.getstack.run/v1/notifications/destinations \
-H "Authorization: Bearer sk_live_op_abc123" \
-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 sk_live_op_abc123" \
-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 sk_live_op_abc123" \
-H "Content-Type: application/json" \
-d '{
"channel_type": "sms",
"destination": "+14155551234"
}'Response - 201 Created
{
"id": "ndst_abc123",
"operator_id": "op_abc123",
"channel_type": "email",
"destination": "alerts@example.com",
"verified": false,
"webhook_secret": null,
"created_at": "2026-04-15T10:00:00.000Z"
}Email destinations are auto-verified if the email matches the operator's registered email. Webhook destinations are auto-verified on creation (they receive a webhook_secret for signature verification). SMS and other email destinations require the two-step verification flow.
Error Responses
- 401 Unauthorized - Missing or invalid Authorization header.
- 409 Conflict - A destination with the same channel_type and destination already exists.
List Destinations
GET /v1/notifications/destinations
curl https://api.getstack.run/v1/notifications/destinations \
-H "Authorization: Bearer sk_live_op_abc123"Delete a Destination
DELETE /v1/notifications/destinations/:id
curl -X DELETE https://api.getstack.run/v1/notifications/destinations/ndst_abc123 \
-H "Authorization: Bearer sk_live_op_abc123"{
"deleted": true
}Verify a Destination
Verification is a two-step process for email and SMS destinations. First, request a verification code. Then submit the code to complete verification.
Step 1: Send Verification Code
POST /v1/notifications/destinations/:id/send-code
curl -X POST https://api.getstack.run/v1/notifications/destinations/ndst_abc123/send-code \
-H "Authorization: Bearer sk_live_op_abc123"Step 2: Submit Verification Code
POST /v1/notifications/destinations/:id/verify
curl -X POST https://api.getstack.run/v1/notifications/destinations/ndst_abc123/verify \
-H "Authorization: Bearer sk_live_op_abc123" \
-H "Content-Type: application/json" \
-d '{
"code": "123456"
}'Response - 200 OK
{
"verified": true
}Error Response - 400 Bad Request
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.
Test a Destination
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 sk_live_op_abc123"Notification Channels (Rules)
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.
Create a Channel Rule
POST /v1/notifications/channels
{
"destination_ids": ["ndst_abc123", "ndst_def456"],
"events": ["passport.flagged", "agent.blocked"],
"min_severity": "warning"
}- destination_id (string, optional) - Single destination ID. Use either this or destination_ids.
- destination_ids (string[], optional) - Array of destination IDs for multi-destination rules. At least one destination is required.
- events (string[], required) - Array of event types to match. At least one required.
- min_severity (string, optional) - Minimum severity to trigger: "info", "warning", or "critical". Default: "warning".
Request Example
curl -X POST https://api.getstack.run/v1/notifications/channels \
-H "Authorization: Bearer sk_live_op_abc123" \
-H "Content-Type: application/json" \
-d '{
"destination_ids": ["ndst_abc123"],
"events": ["passport.flagged", "passport.blocked", "agent.blocked"],
"min_severity": "warning"
}'Response - 201 Created
{
"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"
}Error Responses
- 400 Bad Request - Missing events, unverified destination, or no destination specified.
- 401 Unauthorized - Missing or invalid Authorization header.
- 404 Not Found - Destination ID does not exist or belongs to a different operator.
List Channel Rules
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 sk_live_op_abc123"Update a Channel Rule
PATCH /v1/notifications/channels/:id
{
"destination_ids": ["ndst_abc123", "ndst_ghi789"],
"events": ["passport.flagged", "passport.blocked", "agent.blocked", "passport.missed_checkout"],
"min_severity": "info"
}- destination_ids (string[], optional) - Updated list of destination IDs. All must be verified.
- events (string[], optional) - Updated list of event types.
- min_severity (string, optional) - Updated minimum severity.
Request Example
curl -X PATCH https://api.getstack.run/v1/notifications/channels/nch_abc123 \
-H "Authorization: Bearer sk_live_op_abc123" \
-H "Content-Type: application/json" \
-d '{
"events": ["passport.flagged", "agent.blocked"],
"min_severity": "critical"
}'Delete a Channel Rule
DELETE /v1/notifications/channels/:id
curl -X DELETE https://api.getstack.run/v1/notifications/channels/nch_abc123 \
-H "Authorization: Bearer sk_live_op_abc123"{
"deleted": true
}Supported Event Types
These are the event types you can use in notification channel rules:
- passport.flagged - A passport checkout raised warning or critical flags during review.
- passport.blocked - An agent was blocked from passport issuance due to accountability violations.
- agent.blocked - An agent was blocked (overlaps with passport.blocked for backward compatibility).
- passport.missed_checkout - A passport expired without the agent submitting a checkout report.
When creating rules without specifying events, the system defaults to all four event types: passport.flagged, passport.blocked, agent.blocked, and passport.missed_checkout.
Legacy Channel Creation
For backward compatibility, you can also create channels with inline destination details instead of referencing a destination ID. This creates an unverified channel (unless the type is webhook) that must be verified separately.
{
"channel_type": "email",
"destination": "alerts@example.com",
"events": ["passport.flagged"],
"min_severity": "warning"
}- channel_type (string, required) - "email", "sms", or "webhook".
- destination (string, required) - The email, phone number, or webhook URL.
- events (string[], optional) - Event types to match. Defaults to all events.
- min_severity (string, optional) - Minimum severity. Default: "warning".
- destination_id (string, optional) - Reference to a verified destination.
Legacy Channel Verification
Legacy channels without a destination_id can be verified directly using the same send-code and verify flow:
- POST /v1/notifications/channels/:id/send-code - Send verification code.
- POST /v1/notifications/channels/:id/verify - Submit code to verify.
- POST /v1/notifications/channels/:id/test - Send test notification.
Webhook Payload Format
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",
"timestamp": "2026-04-15T10:05:00.000Z",
"operator_id": "op_abc123",
"severity": "warning",
"data": {
"jti": "ppt_abc123",
"agent_id": "agt_xyz",
"flags": [
{
"type": "undeclared_service",
"severity": "warning",
"message": "Agent accessed slack but did not declare it in intent"
}
]
}
}Signature verification
Every webhook request carries an X-STACK-Signature-256header. 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-256: a7f9…c2e1
signature = hex( HMAC_SHA256(secret = webhook_secret, message = raw_request_body) )Node.js receiver
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-256');
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();
});Python receiver
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-256", "")
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 "", 204Delivery retries
- Webhook failures (non-2xx, timeout) are retried with exponential backoff: 1m, 5m, 30m, 3h
- After four failed attempts the destination is marked unhealthy and rules that target it are paused
- Re-verify the destination (POST .../verify) to resume delivery