STACK
MENU
DOCS / API REFERENCE / NOTIFICATIONS

Notifications API

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.

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. Email is available on every tier; SMS and webhook destinations require a paid tier and return 403 on free.

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 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"
  }'

Response - 201 Created

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

Webhook Creation Response

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

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 YOUR_API_KEY"

Delete a Destination

DELETE /v1/notifications/destinations/:id

bash
curl -X DELETE https://api.getstack.run/v1/notifications/destinations/ndst_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"
json
{
  "deleted": true
}

Rotate a Webhook Secret

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.

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

Verify a Destination

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.

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 YOUR_API_KEY"

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 YOUR_API_KEY" \
  -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 YOUR_API_KEY"

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 YOUR_API_KEY" \
  -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 YOUR_API_KEY"

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 YOUR_API_KEY" \
  -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 YOUR_API_KEY"
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.

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",
  "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"
}

Signature verification

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.

text
X-Stack-Signature: 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');
  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", "")
    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

Webhook Delivery

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

stack | Docs