Skip to content

Webhook Integration

Eric Fitzgerald edited this page Apr 8, 2026 · 6 revisions

Webhook Integration

TMI's webhook system lets you subscribe to events and receive HTTP POST notifications when threat models, diagrams, and other resources change. You can use webhooks to build real-time integrations with external systems.

Common use cases:

  • Notify team channels (Slack, Teams) when threats are identified
  • Create tickets in issue trackers when new threats appear
  • Trigger CI/CD pipelines when threat models change
  • Forward events to SIEM systems for critical threat alerts
  • Archive threat models to external storage
  • Synchronize threat data with other security tools

Architecture

The webhook system uses a worker-based architecture for reliable delivery:

Event Occurs → Redis Stream → Worker Processes → HTTP POST → Your Endpoint
                                                             ↓
                                                   Status Callback (optional)

Components:

Component Responsibility
Event Emitter Publishes events to Redis Streams when resources change
Event Consumer Reads events from the stream and creates delivery records
Challenge Worker Verifies new subscriptions using challenge-response
Delivery Worker Delivers webhooks with retry and exponential backoff
Cleanup Worker Removes old delivery records and inactive subscriptions

See the Webhook Operations Guide for deployment details.

Quick Start

An example webhook application, written in Python and designed to run on AWS Lambda, is available for you to clone or fork as a starting point for your own integrations.

1. Create a Webhook Endpoint

Your endpoint must accept POST requests and respond with 200 OK:

Minimal example (Express.js):

const express = require("express");
const app = express();

app.use(express.json());

app.post("/webhooks/tmi", (req, res) => {
  const event = req.body;
  console.log("Received event:", event.event_type, event);

  // Process event asynchronously
  processEvent(event).catch(console.error);

  // Respond immediately
  res.status(200).send("OK");
});

app.listen(3000);

Important: Respond within 30 seconds to avoid delivery timeouts. Process events asynchronously whenever possible.

2. Register Webhook Subscription

Create a webhook subscription through the API:

curl -X POST https://api.tmi.dev/admin/webhooks/subscriptions \
  -H "Authorization: Bearer $YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Integration",
    "url": "https://your-domain.com/webhooks/tmi",
    "events": ["threat_model.created", "threat_model.updated"],
    "secret": "your-shared-secret-for-hmac"
  }'

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "pending_verification",
  "name": "My Integration",
  "url": "https://your-domain.com/webhooks/tmi",
  "events": ["threat_model.created", "threat_model.updated"],
  "secret": "generated-or-provided-hmac-secret",
  "created_at": "2025-01-15T14:30:00Z",
  "modified_at": "2025-01-15T14:30:00Z"
}

3. Handle the Verification Challenge

After you register a subscription, TMI sends a challenge request to verify that your endpoint is reachable and under your control.

Challenge request:

{
  "type": "webhook.challenge",
  "challenge": "abc123def456"
}

Required response:

{
  "challenge": "abc123def456"
}

Example handler:

app.post("/webhooks/tmi", (req, res) => {
  const event = req.body;

  // Handle challenge
  if (event.type === "webhook.challenge") {
    return res.json({ challenge: event.challenge });
  }

  // Handle other events
  processEvent(event);
  res.status(200).send("OK");
});

After successful verification, your subscription status changes to active and you begin receiving events.

Event Types

All event types follow the {resource}.{action} naming pattern. The complete list of subscribable event types is defined in the OpenAPI schema (see API-Specifications).

Threat Model Events

threat_model.created, threat_model.updated, threat_model.deleted:

{
  "event_type": "threat_model.created",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "object_id": "550e8400-e29b-41d4-a716-446655440000",
  "object_type": "threat_model",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T14:30:00Z",
  "data": {
    "name": "Production API Threat Model",
    "description": "Threat model for production API services"
  }
}

Diagram Events

diagram.created, diagram.updated, diagram.deleted:

{
  "event_type": "diagram.created",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "object_id": "7d8f6e5c-4b3a-2190-8765-fedcba987654",
  "object_type": "diagram",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T14:35:00Z",
  "data": {
    "title": "System Architecture",
    "diagram_type": "data_flow"
  }
}

Document Events

document.created, document.updated, document.deleted:

{
  "event_type": "document.created",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "object_id": "abc-123-def-456",
  "object_type": "document",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T14:40:00Z",
  "data": {
    "name": "Security Requirements.pdf",
    "content_type": "application/pdf",
    "size": 524288
  }
}

Threat Events

threat.created, threat.updated, threat.deleted:

{
  "event_type": "threat.created",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "object_id": "9a8b7c6d-5e4f-3210-abcd-ef1234567890",
  "object_type": "threat",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T14:50:00Z",
  "data": {
    "title": "SQL Injection via API Input",
    "severity": "high"
  }
}

Additional Resource Events

The following resource types also emit created, updated, and deleted events with the same payload structure:

Resource Type Event Prefix object_type
Notes note.* note
Repositories repository.* repository
Assets asset.* asset
Metadata metadata.* metadata
Surveys survey.* survey
Survey Responses survey_response.* survey_response

Addon Events

addon.invoked: Emitted when an add-on is invoked. See Addon-System for details.

{
  "event_type": "addon.invoked",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "object_id": "addon-uuid",
  "object_type": "addon",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T15:00:00Z",
  "data": {}
}

Security

URL Validation

All webhook URLs must meet these requirements:

  • HTTPS required in production. HTTP is allowed only when the server is configured with allowHTTPWebhooks.
  • Valid DNS hostname per RFC 1035/1123/5890. IDN (internationalized) hostnames are supported.
  • Not on the deny list. The URL must not match any entry in the configurable URL deny list.

Deny list: Administrators can populate the webhook_url_deny_list database table with glob or regex patterns to block hostnames. Typical entries block localhost, private IP ranges, link-local addresses, and cloud metadata endpoints. The deny list is checked on every subscription creation. If the list cannot be loaded, the URL is rejected (fail-closed).

HMAC Signature Verification

Always verify HMAC signatures to confirm that webhook payloads originate from TMI and have not been tampered with.

Signature header:

X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592

Verification example (Node.js):

const crypto = require("crypto");

function verifyWebhook(payloadBody, signature, secret) {
  const hmac = crypto.createHmac("sha256", secret);
  hmac.update(payloadBody);
  const expectedSig = `sha256=${hmac.digest("hex")}`;

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSig),
  );
}

app.post("/webhooks/tmi", (req, res) => {
  const signature = req.headers["x-webhook-signature"];
  const payloadBody = JSON.stringify(req.body);

  if (!verifyWebhook(payloadBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  // Process webhook...
  res.status(200).send("OK");
});

Verification example (Python):

import hmac
import hashlib

def verify_webhook(payload_body, signature, secret):
    """Verify webhook HMAC signature"""
    expected = hmac.new(
        secret.encode('utf-8'),
        payload_body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    expected_sig = f"sha256={expected}"

    # Constant-time comparison
    return hmac.compare_digest(signature, expected_sig)

@app.route('/webhooks/tmi', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    payload_body = request.get_data(as_text=True)

    if not verify_webhook(payload_body, signature, WEBHOOK_SECRET):
        return 'Unauthorized', 401

    # Process webhook...
    return 'OK', 200

Request Headers

Every webhook delivery includes the following headers:

POST /webhooks/tmi HTTP/1.1
Host: your-domain.com
Content-Type: application/json
X-Webhook-Event: threat_model.created
X-Webhook-Delivery-Id: 7fa85f64-5717-4562-b3fc-2c963f66afa6
X-Webhook-Subscription-Id: 7d8f6e5c-4b3a-2190-8765-fedcba987654
X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592
User-Agent: TMI-Webhook/1.0

Response Headers for Addon Invocations

When your webhook handles addon invocations (X-Webhook-Event: addon.invoked), you control how TMI processes the completion status by setting the X-TMI-Callback response header:

X-TMI-Callback value Behavior
Not set, or any value except async TMI auto-completes the invocation as completed
async TMI marks the invocation as in_progress and expects your service to call back with status updates

See Addon-System#callback-modes for full details on callback modes.

Delivery Guarantees

Retry Logic

Failed deliveries are retried with exponential backoff:

Attempt Delay
1 Immediate
2 1 minute
3 5 minutes
4 15 minutes
5 30 minutes

After all 5 attempts are exhausted, the delivery is marked failed.

Success Criteria

A delivery is considered successful when:

  • Your endpoint returns an HTTP 2xx status code (200-299)
  • The response arrives within 30 seconds

Idempotency

Because retries can cause duplicate deliveries, use the X-Webhook-Delivery-Id header to detect and skip events you have already processed:

const processedDeliveries = new Set();

app.post("/webhooks/tmi", (req, res) => {
  const deliveryId = req.headers["x-webhook-delivery-id"];

  // Check if already processed
  if (processedDeliveries.has(deliveryId)) {
    console.log("Duplicate delivery, skipping");
    return res.status(200).send("OK");
  }

  // Process webhook
  processEvent(req.body);

  // Mark as processed
  processedDeliveries.add(deliveryId);

  res.status(200).send("OK");
});

Rate Limits

Default Quotas (Per Owner)

Limit Default Value
Max Subscriptions 10
Max Events Per Minute 12
Subscription Requests Per Minute 10
Subscription Requests Per Day 20

Custom Quotas

Administrators can configure custom quotas:

curl -X PUT https://api.tmi.dev/admin/quotas/webhooks/{user_id} \
  -H "Authorization: Bearer $ADMIN_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "max_subscriptions": 50,
    "max_subscription_requests_per_minute": 10,
    "max_subscription_requests_per_day": 500,
    "max_events_per_minute": 1000
  }'

API Endpoints

All webhook management endpoints require admin privileges (JWT authentication).

Subscription Management

Method Endpoint Description
POST /admin/webhooks/subscriptions Create a subscription
GET /admin/webhooks/subscriptions List all subscriptions
GET /admin/webhooks/subscriptions/{webhook_id} Get subscription details
DELETE /admin/webhooks/subscriptions/{webhook_id} Delete a subscription
POST /admin/webhooks/subscriptions/{webhook_id}/test Send a test delivery

Delivery History (Admin)

Method Endpoint Description
GET /admin/webhooks/deliveries List all deliveries
GET /admin/webhooks/deliveries?subscription_id={id} List deliveries filtered by subscription
GET /admin/webhooks/deliveries/{delivery_id} Get delivery details

Delivery Status (Dual Auth: JWT or HMAC)

These endpoints support both JWT authentication and HMAC signature authentication, allowing webhook receivers to query and update delivery status.

Method Endpoint Description
GET /webhook-deliveries/{delivery_id} Get delivery status
POST /webhook-deliveries/{delivery_id}/status Update delivery status (async callback)

Quota Management

Method Endpoint Description
GET /admin/quotas/webhooks List all quotas
GET /admin/quotas/webhooks/{user_id} Get quota for a user
PUT /admin/quotas/webhooks/{user_id} Update quota for a user
DELETE /admin/quotas/webhooks/{user_id} Delete a custom quota

See REST-API-Reference for complete API documentation.

Best Practices

Endpoint Implementation

  1. Respond Quickly: Return 200 OK immediately, process asynchronously

    app.post("/webhooks/tmi", (req, res) => {
      queue.add("process-webhook", req.body);
      res.status(200).send("OK");
    });
  2. Implement Idempotency: Use delivery ID to prevent duplicate processing

  3. Verify Signatures: Always verify HMAC signatures

  4. Handle Errors Gracefully: Return 2xx for success, 4xx/5xx for errors

  5. Log Everything: Log all webhook receipts for debugging

Security

  1. Use HTTPS only. TMI rejects HTTP endpoints in production mode.
  2. Use strong, random secrets (minimum 32 characters).
    # Generate a strong secret
    openssl rand -base64 32
  3. Consider IP allowlisting. Restrict inbound traffic to TMI server IPs.
  4. Validate all incoming data. Do not trust payload contents blindly.
  5. Rate-limit your endpoint. Protect against unexpected traffic spikes.

Monitoring

  1. Track failures. Monitor the publication_failures field on each subscription.
  2. Check last success. Watch the last_successful_use timestamp for staleness.
  3. Query deliveries. Review delivery status for debugging:
    curl https://api.tmi.dev/admin/webhooks/deliveries \
      -H "Authorization: Bearer $TOKEN"
  4. Set up alerts. Alert on high failure rates so you can respond quickly.

Performance

  1. Process webhooks asynchronously. Return 200 OK immediately and handle work in the background.
  2. Use a message queue (RabbitMQ, Redis, SQS) to buffer incoming events.
  3. Batch database operations when processing multiple events.
  4. Cache frequently accessed data to reduce latency.
  5. Scale horizontally. Deploy multiple webhook handler instances behind a load balancer.

Integration Examples

Slack Notifications

Send threat notifications to Slack:

const { WebClient } = require("@slack/web-api");
const slack = new WebClient(process.env.SLACK_TOKEN);

async function notifySlack(event) {
  if (event.event_type === "threat_model.created") {
    await slack.chat.postMessage({
      channel: "#security",
      text: `New threat model created: ${event.data.name}`,
      blocks: [
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*New Threat Model*\n${event.data.name}\n${event.data.description}`,
          },
        },
        {
          type: "actions",
          elements: [
            {
              type: "button",
              text: { type: "plain_text", text: "View in TMI" },
              url: `https://tmi.dev/threat-models/${event.threat_model_id}`,
            },
          ],
        },
      ],
    });
  }
}

Email Notifications

Send email when critical threats are identified:

import smtplib
from email.mime.text import MIMEText

def send_email_notification(event):
    if event['event_type'] == 'threat.created':
        threat = event['data']

        if threat.get('severity') == 'critical':
            msg = MIMEText(f"""
            Critical threat identified:

            Title: {threat['title']}
            Description: {threat['description']}
            Threat Model: {threat['threat_model_name']}

            View in TMI: https://tmi.dev/threats/{event['object_id']}
            """)

            msg['Subject'] = f"[CRITICAL] {threat['title']}"
            msg['From'] = 'security@example.com'
            msg['To'] = 'security-team@example.com'

            with smtplib.SMTP('smtp.example.com', 587) as smtp:
                smtp.starttls()
                smtp.login('username', 'password')
                smtp.send_message(msg)

Logging to SIEM

Forward events to SIEM system:

import requests

def forward_to_siem(event):
    """Forward webhook event to SIEM"""
    siem_endpoint = 'https://siem.example.com/api/events'

    siem_event = {
        'source': 'TMI',
        'event_type': event['event_type'],
        'timestamp': event['timestamp'],
        'severity': map_severity(event),
        'object_id': event['object_id'],
        'object_type': event['object_type'],
        'owner_id': event['owner_id'],
        'data': event['data']
    }

    response = requests.post(
        siem_endpoint,
        json=siem_event,
        headers={'Authorization': f'Bearer {SIEM_TOKEN}'}
    )
    response.raise_for_status()

Troubleshooting

Subscription Not Receiving Events

Verify the following:

  1. Subscription status is active (not pending_verification).
  2. The event types in your subscription match the events being triggered.
  3. Any threat model filter (threat_model_id) is set correctly.
  4. You have not exceeded your rate limit.
  5. Your endpoint is responding with a 2xx status code.

Debug:

# Check subscription status
curl https://api.tmi.dev/admin/webhooks/subscriptions/{id} \
  -H "Authorization: Bearer $TOKEN"

# Check recent deliveries
curl https://api.tmi.dev/admin/webhooks/deliveries \
  -H "Authorization: Bearer $TOKEN"

Verification Failed

Common causes:

  1. Your endpoint does not return the challenge token in the response body.
  2. The response takes longer than 10 seconds (challenge requests time out after 10 seconds).
  3. Your endpoint returns a non-200 status code. The challenge requires exactly 200 OK.
  4. TMI allows a maximum of 3 challenge attempts before marking the subscription for deletion.
  5. Network connectivity issues prevent TMI from reaching your endpoint.

Fix:

// Correct challenge response
app.post("/webhooks/tmi", (req, res) => {
  if (req.body.type === "webhook.challenge") {
    return res.json({ challenge: req.body.challenge });
  }
  // ...
});

High Failure Rate

Diagnose:

# Check failure reasons
curl https://api.tmi.dev/admin/webhooks/deliveries \
  -H "Authorization: Bearer $TOKEN" | jq '.[] | select(.status=="failed")'

Common causes and solutions:

Cause Solution
Endpoint unavailable or slow Improve endpoint reliability and response time
Invalid HMAC signature verification Fix your signature verification logic
Endpoint returning 4xx/5xx errors Return 2xx for successfully processed events
Network issues (firewall, DNS) Check network connectivity and firewall rules

Webhook Operations (for Operators)

This section covers operational management of the webhook system. For complete deployment guidance, see the Webhook Operations Guide.

Worker Configuration

TMI runs four background workers for webhook processing:

Worker Interval Source File
Event Consumer 5-second block api/webhook_event_consumer.go
Challenge Worker 30 seconds api/webhook_challenge_worker.go
Delivery Worker 2 seconds api/webhook_delivery_worker.go
Cleanup Worker 1 hour api/webhook_cleanup_worker.go

Workers start automatically when the server starts with Redis and PostgreSQL available.

Default Quotas

Default per-owner quotas are listed in the Rate Limits section above. The values are defined in api/webhook_store.go.

Redis Configuration

The webhook system uses Redis Streams:

# Check stream exists
XINFO STREAM tmi:events

# Check consumer groups
XINFO GROUPS tmi:events
# Expected group: webhook-consumers

# View rate limit keys
KEYS webhook:ratelimit:sub:minute:*
KEYS webhook:ratelimit:sub:day:*
KEYS webhook:ratelimit:events:minute:*

Storage

Webhook data is split across PostgreSQL and Redis.

PostgreSQL tables (managed by GORM AutoMigrate; model definitions in api/models/models.go):

Table Purpose
webhook_subscriptions Subscription definitions
webhook_quotas Per-owner rate limits
webhook_url_deny_list SSRF prevention patterns

Redis (TTL-based, key pattern webhook:delivery:{uuid}):

  • Delivery records and status (replaced the former webhook_deliveries PostgreSQL table)
  • TTLs: active records expire after 4 hours, terminal records after 7 days

Monitoring Queries

-- Active subscriptions
SELECT COUNT(*) FROM webhook_subscriptions WHERE status = 'active';

-- High failure rate subscriptions
SELECT id, name, url, publication_failures, last_successful_use
FROM webhook_subscriptions
WHERE status = 'active' AND publication_failures > 5
ORDER BY publication_failures DESC;

Delivery status is also available through the admin API endpoints described in the API Endpoints section above.

Log Patterns

Use these patterns to monitor webhook activity in the TMI log:

Pattern What it shows
webhook-consumer Event consumer activity
challenge worker Challenge verification attempts
delivering webhook Delivery attempts
ERROR.*webhook Worker errors
# Example: find delivery errors
grep "ERROR.*webhook" /var/log/tmi/tmi.log

Related Documentation

Wiki pages:

External guides:

Clone this wiki locally