-
Notifications
You must be signed in to change notification settings - Fork 0
API Integration
This guide provides comprehensive information for integrating with TMI's REST API, WebSocket API, and building client applications.
- REST API Integration
- WebSocket API Integration
- Client Integration Patterns
- Authentication Integration
- Error Handling
- Best Practices
TMI provides a RESTful API with OpenAPI 3.0 specification.
Base URL: http://localhost:8080 (development) or https://api.tmi.dev (production)
API Specification: /docs/reference/apis/tmi-openapi.json
Content Type: application/json
Authentication: Bearer token (JWT)
# Get server info (no auth required)
curl http://localhost:8080/
# Get threat models (requires auth)
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/threat_modelsList Threat Models:
GET /threat_models
Authorization: Bearer {token}Response:
[
{
"id": "uuid",
"name": "Web Application Security",
"description": "Security analysis for web app",
"owner": "alice@example.com",
"created_at": "2025-01-15T10:00:00Z",
"modified_at": "2025-01-15T15:30:00Z",
"diagram_count": 3,
"threat_count": 12,
"document_count": 2
}
]Create Threat Model:
POST /threat_models
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "My Threat Model",
"description": "Security analysis for new feature"
}Important: Do NOT include calculated or server-controlled fields:
- ❌
id(server-generated) - ❌
created_at,modified_at(server-set) - ❌
owner,created_by(from JWT token) - ❌
diagram_count,threat_count,document_count(calculated)
Update Threat Model:
PUT /threat_models/{threat_model_id}
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "Updated Name",
"description": "Updated description"
}Patch Threat Model:
PATCH /threat_models/{threat_model_id}
Authorization: Bearer {token}
Content-Type: application/json-patch+json
[
{
"op": "replace",
"path": "/name",
"value": "New Name"
}
]Delete Threat Model:
DELETE /threat_models/{threat_model_id}
Authorization: Bearer {token}Create Diagram:
POST /threat_models/{threat_model_id}/diagrams
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "System Architecture",
"description": "High-level data flow diagram"
}Get Diagram:
GET /threat_models/{threat_model_id}/diagrams/{diagram_id}
Authorization: Bearer {token}Response:
{
"id": "uuid",
"threat_model_id": "uuid",
"name": "System Architecture",
"description": "High-level data flow diagram",
"cells": [
{
"id": "cell-uuid",
"shape": "process",
"x": 100,
"y": 200,
"width": 120,
"height": 80,
"label": "Authentication Service"
}
],
"update_vector": 5
}Update Diagram Cells:
PUT /diagrams/{diagram_id}/cells
Authorization: Bearer {token}
Content-Type: application/json
{
"cells": [
{
"id": "cell-uuid",
"shape": "process",
"x": 150,
"y": 250,
"width": 120,
"height": 80,
"label": "Updated Label"
}
]
}Note: Position and size accept both formats:
- Flat:
{x: 100, y: 200, width: 80, height: 60} - Nested (legacy):
{position: {x: 100, y: 200}, size: {width: 80, height: 60}}
API always returns flat format.
Create Threat:
POST /threat_models/{threat_model_id}/threats
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "SQL Injection",
"description": "Database injection vulnerability",
"stride": "tampering",
"severity": "high",
"status": "open",
"mitigation": "Use parameterized queries"
}List Threats:
GET /threat_models/{threat_model_id}/threats
Authorization: Bearer {token}All entities support arbitrary key-value metadata:
Create Metadata:
POST /threat_models/{threat_model_id}/metadata
Authorization: Bearer {token}
Content-Type: application/json
{
"key": "project_phase",
"value": "design"
}List Metadata:
GET /threat_models/{threat_model_id}/metadata
Authorization: Bearer {token}Update Metadata:
PUT /threat_models/{threat_model_id}/metadata/{key}
Authorization: Bearer {token}
Content-Type: application/json
{
"value": "implementation"
}Delete Metadata:
DELETE /threat_models/{threat_model_id}/metadata/{key}
Authorization: Bearer {token}Add Authorization Entry:
POST /threat_models/{threat_model_id}/authorization
Authorization: Bearer {token}
Content-Type: application/json
{
"subject": "bob@example.com",
"subject_type": "user",
"role": "writer"
}Grant Public Read Access:
POST /threat_models/{threat_model_id}/authorization
Authorization: Bearer {token}
Content-Type: application/json
{
"subject": "everyone",
"subject_type": "group",
"role": "reader"
}Update User Role:
PUT /threat_models/{threat_model_id}/authorization/{index}
Authorization: Bearer {token}
Content-Type: application/json
{
"role": "owner"
}Remove Authorization:
DELETE /threat_models/{threat_model_id}/authorization/{index}
Authorization: Bearer {token}| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Resource created successfully |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input data |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource not found |
| 409 | Conflict | Duplicate resource or state conflict |
| 422 | Unprocessable Entity | Validation error |
| 500 | Internal Server Error | Server error |
TMI provides WebSocket-based real-time collaboration for simultaneous diagram editing.
CRITICAL: Must join via REST API before WebSocket connection.
Create Session (as host):
POST /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate
Authorization: Bearer {token}Response (201 Created):
{
"session_id": "uuid",
"host": "alice@example.com",
"threat_model_id": "uuid",
"threat_model_name": "Web App Security",
"diagram_id": "uuid",
"diagram_name": "Architecture Diagram",
"participants": [
{
"user_id": "alice@example.com",
"joined_at": "2025-01-15T10:00:00Z",
"permissions": "writer"
}
],
"websocket_url": "ws://localhost:8080/threat_models/.../diagrams/.../ws"
}Join Existing Session:
PUT /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate
Authorization: Bearer {token}Response (200 OK): Same structure as create
Check Session Status:
GET /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate
Authorization: Bearer {token}Use the websocket_url from the session response:
const ws = new WebSocket(`${websocket_url}?token=${jwt_token}`);
ws.onopen = () => {
console.log('Connected to collaboration session');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};CRITICAL: First message received is diagram_state_sync:
{
"message_type": "diagram_state_sync",
"diagram_id": "uuid",
"update_vector": 42,
"cells": [ /* current diagram state */ ]
}Always handle this message to prevent "cell_already_exists" errors:
function handleDiagramStateSync(message) {
// Compare with locally cached diagram
const localVector = cachedDiagram?.update_vector || 0;
const serverVector = message.update_vector || 0;
if (serverVector !== localVector) {
console.warn('State mismatch - resyncing');
// Update local state with server cells
cachedDiagram.cells = message.cells;
cachedDiagram.update_vector = message.update_vector;
renderDiagram(message.cells);
}
isStateSynchronized = true;
}Diagram Operation (cell add/update/remove):
{
"message_type": "diagram_operation",
"user_id": "alice@example.com",
"operation_id": "uuid",
"operation": {
"type": "patch",
"cells": [
{
"id": "cell-uuid",
"operation": "add",
"data": {
"shape": "process",
"x": 100,
"y": 200,
"width": 120,
"height": 80,
"label": "New Process"
}
}
]
}
}Request Presenter Mode:
{
"message_type": "presenter_request",
"user_id": "alice@example.com"
}Send Cursor Position (only if presenter):
{
"message_type": "presenter_cursor",
"user_id": "alice@example.com",
"cursor_position": { "x": 150, "y": 300 }
}Undo Request:
{
"message_type": "undo_request",
"user_id": "alice@example.com"
}Redo Request:
{
"message_type": "redo_request",
"user_id": "alice@example.com"
}Diagram Operation (from other users):
{
"message_type": "diagram_operation",
"user_id": "bob@example.com",
"operation_id": "uuid",
"operation": {
"type": "patch",
"cells": [ /* changes */ ]
}
}Presenter Changed:
{
"message_type": "current_presenter",
"current_presenter": "alice@example.com"
}Presenter Cursor:
{
"message_type": "presenter_cursor",
"user_id": "alice@example.com",
"cursor_position": { "x": 150, "y": 300 }
}User Joined:
{
"event": "join",
"user_id": "charlie@example.com",
"timestamp": "2025-01-15T10:05:00Z"
}User Left:
{
"event": "leave",
"user_id": "bob@example.com",
"timestamp": "2025-01-15T10:10:00Z"
}Session Ended:
{
"event": "session_ended",
"user_id": "alice@example.com",
"message": "Session ended: host has left",
"timestamp": "2025-01-15T10:15:00Z"
}State Correction (conflict detected):
{
"message_type": "state_correction",
"update_vector": 45
}Authorization Denied:
{
"message_type": "authorization_denied",
"original_operation_id": "uuid",
"reason": "insufficient_permissions"
}CRITICAL: Never send WebSocket messages when applying remote operations.
class DiagramCollaborationManager {
constructor(diagramEditor) {
this.isApplyingRemoteChange = false;
// Listen to local diagram changes
this.diagramEditor.on('cellChanged', (change) => {
if (this.isApplyingRemoteChange) {
return; // DON'T send WebSocket message
}
this.sendOperation(change); // Only send for local changes
});
}
handleDiagramOperation(message) {
// Skip own operations
if (message.user_id === this.currentUser.email) {
return;
}
this.isApplyingRemoteChange = true;
try {
this.applyOperationToEditor(message.operation);
} finally {
this.isApplyingRemoteChange = false;
}
}
}class TMIClient {
private apiUrl: string;
private token: string;
constructor(apiUrl: string, token: string) {
this.apiUrl = apiUrl;
this.token = token;
}
// REST API Methods
async getThreatModels(): Promise<ThreatModel[]> {
const response = await fetch(`${this.apiUrl}/threat_models`, {
headers: { Authorization: `Bearer ${this.token}` }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
async createThreatModel(data: CreateThreatModelRequest): Promise<ThreatModel> {
const response = await fetch(`${this.apiUrl}/threat_models`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
// WebSocket Collaboration
async startCollaboration(tmId: string, diagramId: string): Promise<WebSocket> {
// 1. Join session via REST API
const session = await this.joinCollaborationSession(tmId, diagramId);
// 2. Connect to WebSocket
const ws = new WebSocket(`${session.websocket_url}?token=${this.token}`);
// 3. Set up handlers
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleWebSocketMessage(message);
};
return ws;
}
private async joinCollaborationSession(tmId: string, diagramId: string) {
const response = await fetch(
`${this.apiUrl}/threat_models/${tmId}/diagrams/${diagramId}/collaborate`,
{
method: 'POST',
headers: { Authorization: `Bearer ${this.token}` }
}
);
if (response.status === 409) {
// Session exists, join instead
return this.joinExistingSession(tmId, diagramId);
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
}import requests
import websocket
import json
class TMIClient:
def __init__(self, api_url, token):
self.api_url = api_url
self.token = token
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {token}'
})
def get_threat_models(self):
response = self.session.get(f'{self.api_url}/threat_models')
response.raise_for_status()
return response.json()
def create_threat_model(self, name, description):
response = self.session.post(
f'{self.api_url}/threat_models',
json={'name': name, 'description': description}
)
response.raise_for_status()
return response.json()
def start_collaboration(self, tm_id, diagram_id):
# Join session
session = self.join_collaboration_session(tm_id, diagram_id)
# Connect to WebSocket
ws = websocket.WebSocketApp(
f"{session['websocket_url']}?token={self.token}",
on_message=self.on_websocket_message,
on_open=self.on_websocket_open
)
return ws
def on_websocket_message(self, ws, message):
data = json.loads(message)
if data['message_type'] == 'diagram_state_sync':
self.handle_state_sync(data)
elif data['message_type'] == 'diagram_operation':
self.handle_diagram_operation(data)GET /oauth2/authorize?idp=googleServer redirects to Google OAuth, then back to TMI with authorization code.
TMI processes the callback and returns tokens:
GET /oauth2/callback?code=AUTH_CODE&state=STATEResponse:
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 86400
}Include in Authorization header for all API requests:
Authorization: Bearer eyJhbGc...Get User Info:
GET /oauth2/userinfo
Authorization: Bearer {token}Response:
{
"sub": "google:123456789",
"email": "alice@example.com",
"name": "Alice Smith",
"idp": "google"
}Logout:
POST /oauth2/logout
Authorization: Bearer {token}For development, use the test OAuth provider:
# Random test user
curl "http://localhost:8080/oauth2/authorize?idp=test"
# Specific test user
curl "http://localhost:8080/oauth2/authorize?idp=test&login_hint=alice"All errors return JSON with consistent structure:
{
"error": "validation_error",
"message": "Invalid input data",
"details": {
"field": "name",
"reason": "Field is required"
}
}Authorization Denied:
{
"message_type": "authorization_denied",
"original_operation_id": "uuid",
"reason": "insufficient_permissions"
}State Correction:
{
"message_type": "state_correction",
"update_vector": 45
}Handle by re-fetching diagram via REST API:
async function handleStateCorrection(message) {
console.warn('State correction received, resyncing...');
const diagram = await fetch(
`/threat_models/${tmId}/diagrams/${diagramId}`,
{ headers: { Authorization: `Bearer ${token}` } }
).then(r => r.json());
cachedDiagram = diagram;
renderDiagram(diagram.cells);
}async function apiCallWithRetry(url, options, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
// Don't retry client errors (4xx except 429)
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
throw new Error(`HTTP ${response.status}`);
}
// Retry server errors and rate limits
if (attempt < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}- Store tokens securely: Use httpOnly cookies or secure storage
- Refresh tokens: Implement token refresh before expiration
- Handle 401: Redirect to login on authentication failure
- Logout properly: Call logout endpoint to invalidate tokens
- Use appropriate methods: GET for reads, POST for creates, PUT/PATCH for updates
- Handle errors: Check response status and parse error messages
- Validate input: Client-side validation before sending to API
- Paginate large lists: Use pagination parameters when available
- Don't send calculated fields: Let server compute counts and timestamps
- Join via REST first: Always join collaboration session via REST API before WebSocket
-
Handle state sync: Process initial
diagram_state_syncmessage - Prevent echo: Don't send WebSocket messages for remote changes
- Graceful reconnection: Implement exponential backoff for reconnection
- Update progress: Send progress updates for presenter mode
- Handle disconnection: Save state before disconnecting
- Throttle high-frequency events: Throttle cursor updates (100ms), debounce selection (250ms)
- Batch operations: Use batch endpoints when creating multiple items
- Cache responses: Cache GET responses with appropriate TTL
- Use WebSockets wisely: Only for real-time collaboration, not for all updates
- Retry transient errors: Implement exponential backoff for 500/503 errors
- Don't retry 4xx: Client errors should not be retried
- Show user-friendly messages: Parse error details and show helpful messages
- Log errors: Log errors for debugging but don't expose sensitive info to users
- Validate on client and server: Never trust client-side validation alone
- Sanitize user input: Prevent XSS and injection attacks
- Use HTTPS: Always use TLS in production
- Check permissions: Verify user has required role before operations
See /docs/developer/integration/ in the TMI repository for complete code examples:
-
client-integration-guide.md- Comprehensive WebSocket integration -
client-oauth-integration.md- OAuth client patterns -
collaborative-editing-plan.md- Real-time editing implementation
For complete API documentation:
-
OpenAPI Spec:
/docs/reference/apis/tmi-openapi.json -
WebSocket Spec:
/docs/reference/apis/tmi-asyncapi.yml -
Server Endpoint:
http://localhost:8080/(server info and version)
- Testing - Learn testing strategies
- Extending TMI - Build addons and integrations
- Architecture and Design - Understand the architecture
- Using TMI for Threat Modeling
- Accessing TMI
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Metadata and Extensions
- Planning Your Deployment
- Deploying TMI Server
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Component Integration
- Post-Deployment
- Monitoring and Health
- Database Operations
- Security Operations
- Performance and Scaling
- Maintenance Tasks