-
Notifications
You must be signed in to change notification settings - Fork 2
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
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.
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.
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.
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"
}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.
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.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.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.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.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"
}
}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.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": {}
}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).
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', 200Every 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.0When 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.
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.
A delivery is considered successful when:
- Your endpoint returns an HTTP 2xx status code (200-299)
- The response arrives within 30 seconds
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");
});| Limit | Default Value |
|---|---|
| Max Subscriptions | 10 |
| Max Events Per Minute | 12 |
| Subscription Requests Per Minute | 10 |
| Subscription Requests Per Day | 20 |
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
}'All webhook management endpoints require admin privileges (JWT authentication).
| 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 |
| 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 |
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) |
| 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.
-
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"); });
-
Implement Idempotency: Use delivery ID to prevent duplicate processing
-
Verify Signatures: Always verify HMAC signatures
-
Handle Errors Gracefully: Return 2xx for success, 4xx/5xx for errors
-
Log Everything: Log all webhook receipts for debugging
- Use HTTPS only. TMI rejects HTTP endpoints in production mode.
-
Use strong, random secrets (minimum 32 characters).
# Generate a strong secret openssl rand -base64 32 - Consider IP allowlisting. Restrict inbound traffic to TMI server IPs.
- Validate all incoming data. Do not trust payload contents blindly.
- Rate-limit your endpoint. Protect against unexpected traffic spikes.
-
Track failures. Monitor the
publication_failuresfield on each subscription. -
Check last success. Watch the
last_successful_usetimestamp for staleness. -
Query deliveries. Review delivery status for debugging:
curl https://api.tmi.dev/admin/webhooks/deliveries \ -H "Authorization: Bearer $TOKEN" - Set up alerts. Alert on high failure rates so you can respond quickly.
- Process webhooks asynchronously. Return 200 OK immediately and handle work in the background.
- Use a message queue (RabbitMQ, Redis, SQS) to buffer incoming events.
- Batch database operations when processing multiple events.
- Cache frequently accessed data to reduce latency.
- Scale horizontally. Deploy multiple webhook handler instances behind a load balancer.
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}`,
},
],
},
],
});
}
}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)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()Verify the following:
- Subscription status is
active(notpending_verification). - The event types in your subscription match the events being triggered.
- Any threat model filter (
threat_model_id) is set correctly. - You have not exceeded your rate limit.
- 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"Common causes:
- Your endpoint does not return the challenge token in the response body.
- The response takes longer than 10 seconds (challenge requests time out after 10 seconds).
- Your endpoint returns a non-200 status code. The challenge requires exactly
200 OK. - TMI allows a maximum of 3 challenge attempts before marking the subscription for deletion.
- 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 });
}
// ...
});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 |
This section covers operational management of the webhook system. For complete deployment guidance, see the Webhook Operations Guide.
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 per-owner quotas are listed in the Rate Limits section above. The values are defined in api/webhook_store.go.
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:*
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_deliveriesPostgreSQL table) - TTLs: active records expire after 4 hours, terminal records after 7 days
-- 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.
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.logWiki pages:
- Addon-System -- Webhook-based add-on system
- Issue-Tracker-Integration -- Integrate with issue trackers
- REST-API-Reference -- Complete API documentation
- API-Specifications -- OpenAPI schema and event type definitions
- Database-Operations -- Database management
- Monitoring-and-Health -- System monitoring
External guides:
- Using TMI for Threat Modeling
- Accessing TMI
- Authentication
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Timmy AI Assistant
- Metadata and Extensions
- Planning Your Deployment
- Terraform Deployment (AWS, OCI, GCP, Azure)
- Deploying TMI Server
- OCI Container Deployment
- Certificate Automation
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Component Integration
- Post-Deployment
- Branding and Customization
- Monitoring and Health
- Cloud Logging
- Database Operations
- Security Operations
- Performance and Scaling
- Maintenance Tasks
- Getting Started with Development
- Architecture and Design
- API Integration
- Testing
- Contributing
- Extending TMI
- Dependency Upgrade Plans
- DFD Graphing Library Reference
- Migration Instructions