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

json
{
  "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

bash
# 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

json
{
  "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

bash
curl https://api.getstack.run/v1/notifications/destinations \
  -H "Authorization: Bearer sk_live_op_abc123"

Delete a Destination

DELETE /v1/notifications/destinations/:id

bash
curl -X DELETE https://api.getstack.run/v1/notifications/destinations/ndst_abc123 \
  -H "Authorization: Bearer sk_live_op_abc123"
json
{
  "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

bash
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

bash
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

json
{
  "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.

bash
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

json
{
  "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

bash
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

json
{
  "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.

bash
curl https://api.getstack.run/v1/notifications/channels \
  -H "Authorization: Bearer sk_live_op_abc123"

Update a Channel Rule

PATCH /v1/notifications/channels/:id

json
{
  "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

bash
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

bash
curl -X DELETE https://api.getstack.run/v1/notifications/channels/nch_abc123 \
  -H "Authorization: Bearer sk_live_op_abc123"
json
{
  "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.

json
{
  "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.

json
{
  "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.

text
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.

javascript
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

python
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 "", 204

Delivery 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
stack | docs