API Reference¶
Thalian exposes a set of backend API endpoints via Cloudflare Pages Functions. All endpoints require authentication and are scoped to your workspace.
Authentication¶
Every API request must include a valid Supabase session token in the Authorization header:
Authorization: Bearer <your-session-token>
Tokens are issued by Supabase Auth on login. If your workspace has IP allowlisting enabled in Settings → Security, requests from non-allowed IPs are rejected.
Authorization¶
All endpoints enforce role-based access control. The backend calls verifyPermission() on every request, checking both workspace membership and role-specific permissions before processing. Unauthorized requests receive a 403 response with no internal details.
For a full breakdown of role permissions, see Settings & Admin.
Base URL¶
All API endpoints are served from:
https://app.thalian.ai/api/
Response Format¶
All responses are Content-Type: application/json. Successful responses return the documented fields. Error responses always follow this shape:
{
"error": "Human-readable description of the error"
}
No stack traces, internal paths, or credential fragments are ever included in error responses.
Common Field Types¶
| Type | Format |
|---|---|
| UUID | RFC 4122 — xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
| Timestamp | ISO 8601 UTC — 2026-04-15T12:30:00Z |
| Severity | "critical" | "high" | "medium" | "low" | "info" |
| Plan | "free" | "trial" | "pro" | "enterprise" |
| Identity status | "active" | "suspended" | "deprovisioned" |
| Integration status | "connected" | "disconnected" | "error" | "paused" | "pending" |
MCP API (API key auth)¶
Thalian exposes a separate set of endpoints under /api/mcp/ that accept API keys instead of Supabase session tokens. These are designed for programmatic access from external tools like Claude Code via the MCP server.
API key authentication¶
Generate a key in Settings → API Keys. Pass it as a Bearer token:
Authorization: Bearer thal_your_key_here
API keys are workspace-scoped. Read-only keys (the default) can call all query tools but cannot modify workspace data. Write-scope keys are required for action tools and are recorded in the audit log on every mutating call. They do not require Supabase Auth and work from any environment that can reach mcp.thalian.ai.
MCP endpoints¶
All MCP endpoints accept an optional workspaceId query parameter. If omitted, the workspace associated with the API key is used. Endpoints marked write require a write-scope API key.
| Method | Path | Description | Scope |
|---|---|---|---|
GET |
/api/mcp/risk-score |
Risk score (0–100), severity breakdown, top 5 open findings | read |
GET |
/api/mcp/findings |
Open findings — filterable by severity, category, limit |
read |
GET |
/api/mcp/identity |
Identity details by email — status, MFA, app access, linked findings |
read |
GET |
/api/mcp/integrations |
Connected platforms and sync status | read |
GET |
/api/mcp/posture-summary |
Plain-language executive summary with key metrics | read |
GET |
/api/mcp/rules |
Active detection rules with severity, category, and affected entity types | read |
GET |
/api/mcp/app-policy |
Application policy status by app_name — sanctioned, unauthorized, blocked, or none |
read |
POST |
/api/mcp/sync |
Trigger a background sync for all connected integrations | write |
POST |
/api/mcp/findings/snooze |
Snooze an open finding for 1–90 days | write |
POST |
/api/mcp/findings/dismiss |
Dismiss an open finding | write |
POST |
/api/mcp/findings/remediate |
Queue a remediation action for an open finding | write |
POST |
/api/mcp/app-policy |
Set an application's policy status | write |
GET /api/mcp/risk-score¶
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | No | Defaults to key's workspace |
Response:
{
"score": 72,
"label": "High",
"severity_counts": {
"critical": 3,
"high": 14,
"medium": 28,
"low": 41
},
"total_findings": 86,
"top_risks": [
{
"title": "string",
"severity": "critical",
"category": "string",
"affected_count": 5
}
]
}
label is one of "Low", "Medium", "High", or "Critical".
GET /api/mcp/findings¶
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | No | Defaults to key's workspace |
severity |
string | No | Comma-separated filter: "critical,high" |
category |
string | No | Single category name |
limit |
integer | No | 1–100, default 25 |
Response:
{
"findings": [
{
"id": "uuid",
"title": "string",
"severity": "high",
"category": "string",
"status": "open",
"rule_id": "string",
"affected_entities": {},
"recommended_action": "string",
"created_at": "2026-04-15T12:00:00Z"
}
],
"total": 86,
"returned": 25
}
GET /api/mcp/identity¶
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | No | Defaults to key's workspace |
email |
string | Yes | URL-encoded email address. Case-insensitive. |
Response:
{
"email": "alice@example.com",
"display_name": "string",
"status": "active",
"identity_type": "user",
"mfa_enabled": true,
"last_login_at": "2026-04-10T08:00:00Z",
"department": "Engineering",
"manager_email": "bob@example.com",
"platforms": ["okta", "google_workspace"],
"findings": [
{
"title": "string",
"severity": "high",
"category": "string"
}
],
"app_access": [
{
"app_name": "string",
"role": "string",
"last_used_at": "2026-04-12T14:00:00Z"
}
]
}
identity_type is one of "user", "admin", "external", or "service_account". Returns 404 if no identity with that email exists in the workspace.
GET /api/mcp/integrations¶
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | No | Defaults to key's workspace |
Response:
{
"integrations": [
{
"platform": "okta",
"category": "idp",
"status": "connected",
"last_sync_at": "2026-04-15T11:00:00Z",
"idp_role": "primary_idp"
}
],
"connected_count": 4,
"total_count": 6
}
idp_role is present only for identity providers and is one of "primary_idp" or "directory".
GET /api/mcp/posture-summary¶
Returns a deterministic plain-language summary — no AI call is made.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | No | Defaults to key's workspace |
Response:
{
"summary": "string",
"metrics": {
"risk_score": 72,
"risk_label": "High",
"total_findings": 86,
"severity_counts": {
"critical": 3,
"high": 14,
"medium": 28,
"low": 41
},
"mfa_coverage_pct": 84,
"admin_pct": 12,
"sso_coverage_pct": 91,
"shadow_it_count": 7,
"connected_integrations": 4,
"identity_count": 120
}
}
Coverage metrics (mfa_coverage_pct, admin_pct, sso_coverage_pct, shadow_it_count) are omitted when the data is unavailable for the workspace.
POST /api/mcp/sync¶
Triggers a background sync for all connected integrations. Returns immediately — results appear within minutes.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | No | Defaults to key's workspace |
Response:
{
"status": "triggered",
"integrations_queued": 4,
"platforms": ["okta", "google_workspace", "github", "aws"],
"message": "Sync triggered for 4 integrations"
}
status is "triggered" when at least one integration was queued, or "no_integrations" when the workspace has no connected integrations.
API key management¶
| Method | Path | Description | Min role |
|---|---|---|---|
POST |
/api/api-keys |
Create a new API key (returned once in plaintext) | Admin |
GET |
/api/api-keys |
List active keys (prefix and metadata only — full key never returned) | Admin |
DELETE |
/api/api-keys |
Revoke a key immediately | Admin |
GET /api/api-keys — query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | Workspace to list keys for |
GET response:
{
"keys": [
{
"id": "uuid",
"name": "string",
"prefix": "thal_abc1",
"created_at": "2026-01-10T09:00:00Z",
"last_used_at": "2026-04-14T16:30:00Z"
}
]
}
POST /api/api-keys — request body:
| Field | Type | Required | Constraints |
|---|---|---|---|
workspaceId |
UUID | Yes | |
name |
string | Yes | 1–100 characters. Max 10 active keys per workspace. |
POST response:
{
"key": "thal_xxxxxxxxxxxxxxxxxxxx",
"prefix": "thal_xxxx",
"name": "string",
"created_at": "2026-04-15T12:00:00Z"
}
The full key is returned once and never stored in plaintext. Save it immediately.
DELETE /api/api-keys — request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | |
keyId |
UUID | Yes | ID of the key to revoke |
DELETE response:
{
"revoked": true,
"revoked_at": "2026-04-15T12:00:00Z"
}
Endpoints¶
Health¶
| Method | Path | Description | Min role |
|---|---|---|---|
GET |
/api/health |
Check platform and database connectivity | — (public) |
Response:
{
"status": "ok",
"timestamp": "2026-04-15T12:00:00Z",
"services": {
"database": "ok"
},
"latencyMs": 12
}
status is "ok" or "degraded". services.database is "ok", "degraded", or "unreachable". Returns 503 when the database is unreachable.
Analysis¶
| Method | Path | Description | Min role |
|---|---|---|---|
POST |
/api/analyze |
Trigger an analysis run for the workspace | Agent |
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes |
Response:
{
"findings": [
{
"id": "uuid",
"title": "string",
"severity": "high",
"category": "string",
"status": "open",
"rule_id": "string",
"affected_entities": {},
"affected_identity_id": "uuid",
"affected_application_id": "uuid",
"affected_device_id": "uuid",
"recommended_action": "string",
"action_type": "string",
"finding_key": "string",
"source_integrations": ["okta"],
"confidence": 0.95,
"created_at": "2026-04-15T12:00:00Z"
}
],
"risk_score": 72,
"severity_counts": {
"critical": 3,
"high": 14,
"medium": 28,
"low": 41
},
"analysis_duration_ms": 1240,
"last_synced_at": "2026-04-15T11:00:00Z"
}
affected_identity_id, affected_application_id, and affected_device_id are present only when the finding is linked to a specific entity.
Additional error conditions:
| Code | Condition |
|---|---|
409 |
Analysis already in progress — rate limited to 1 run per 60 seconds per workspace |
AI Chat¶
| Method | Path | Description | Min role |
|---|---|---|---|
POST |
/api/ai-chat |
Send a message to the AI assistant with workspace context | Viewer |
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | |
message |
string | Yes | The user message |
sessionId |
UUID | No | Conversation session ID for multi-turn context |
fileBase64 |
string | No | Base64-encoded file attachment. Max 10 MB. |
fileName |
string | No | Filename for the attachment (used for MIME detection) |
confirmationToken |
string | No | Required when approving a high-risk action returned in a previous response |
Supported attachment MIME types: application/pdf, image/jpeg, image/png, image/webp, image/gif, text/plain.
Response:
{
"response": "string",
"toolCalls": [
{
"id": "string",
"name": "suspend_user",
"input": {},
"requiresConfirmation": true
}
],
"tokens_used": 1840,
"model": "claude-opus-4-7",
"stop_reason": "end_turn"
}
toolCalls is present only when the AI invoked one or more tools. stop_reason is one of "end_turn", "tool_use", or "max_tokens".
Confirmation flow: When a tool call has requiresConfirmation: true, re-send the same request with the confirmationToken field populated to approve execution. Confirmation tokens expire after 5 minutes.
Tool actions:
Actions are split into two tiers:
- Immediate (no confirmation required):
acknowledge_finding,dismiss_finding,snooze_finding,star_finding,sanction_app,create_ticket,trigger_sync - Requires confirmation:
suspend_user,force_password_change,revoke_sessions,revoke_oauth_token,contain_host,block_app,create_access_review
Additional error conditions:
| Code | Condition |
|---|---|
429 |
Anthropic API rate limit exceeded |
503 |
Anthropic API unavailable (retried 3× with exponential backoff) |
Integrations¶
| Method | Path | Description | Min role |
|---|---|---|---|
POST |
/api/connect-integration |
Connect a new integration | Admin |
POST |
/api/sync-integration |
Trigger a manual sync for a connected integration | Admin |
POST |
/api/disconnect-integration |
Remove an integration from the workspace | Admin |
POST /api/connect-integration¶
Integration credentials are validated against the provider's API before being saved. Credentials are encrypted with AES-256-GCM before storage — plaintext credentials are never persisted.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | |
platform |
string | Yes | Platform identifier, e.g. "okta", "google_workspace" |
config |
object | Yes | Platform-specific credentials object. Fields vary by platform — see Integrations Guide for required fields per platform. |
Response:
{
"success": true,
"integrationId": "uuid"
}
Additional error conditions:
| Code | Condition |
|---|---|
403 |
Integration limit reached for your plan |
404 |
Platform not supported |
422 |
Credential validation failed — invalid domain, API key, or credentials rejected by the provider |
POST /api/sync-integration¶
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | |
integrationId |
UUID | Yes | ID of the integration to sync |
Response:
{
"status": "completed",
"platform": "okta",
"entities_synced": {
"identities": 120,
"applications": 34,
"devices": 88,
"entitlements": 410,
"audit_events": 2500
},
"duration_ms": 3820,
"next_auto_sync_at": "2026-04-15T14:00:00Z"
}
status is "queued", "in_progress", or "completed".
Additional error conditions:
| Code | Condition |
|---|---|
404 |
Integration not found |
409 |
Sync already in progress for this integration |
POST /api/disconnect-integration¶
Removes the integration, wipes credentials, and cancels any pending remediation actions for that platform. Existing findings are retained — they remain open but will not be refreshed.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | |
integrationId |
UUID | Yes | ID of the integration to disconnect |
Response:
{
"success": true,
"platform": "okta",
"disconnected_at": "2026-04-15T12:00:00Z",
"pending_actions_cancelled": 2
}
Additional error conditions:
| Code | Condition |
|---|---|
404 |
Integration not found |
Remediation¶
| Method | Path | Description | Min role |
|---|---|---|---|
POST |
/api/execute-action |
Execute a remediation action against a target platform | Agent |
Approval queue: Actions initiated by Agent-role users on high or critical severity findings enter a pending approval queue. Security Analysts, Admins, and Super Admins can execute without a second approver.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | |
actionType |
string | Yes | The remediation action to execute |
targetPlatform |
string | Yes | Platform to act on, e.g. "okta", "google_workspace" |
targetEntityId |
string | Yes | ID of the entity to act on (user ID, app ID, etc.) |
targetEntityLabel |
string | No | Human-readable label for audit log display |
riskId |
UUID | No | Finding ID this action is resolving |
extraParams |
object | No | Action-specific additional parameters |
reason |
string | No | Reason for the action, recorded in the audit log |
Response:
{
"success": true,
"actionId": "uuid",
"result": {}
}
result shape varies by actionType. All executions are recorded in the audit log.
Additional error conditions:
| Code | Condition |
|---|---|
403 |
Trial expired |
404 |
Action not found or confirmation token expired |
Export¶
| Method | Path | Description | Min role |
|---|---|---|---|
POST |
/api/export-audit-log |
Export the full audit log as JSON | Auditor |
POST |
/api/export-workspace |
Export all workspace data | Admin |
POST /api/export-audit-log¶
Enterprise plan only. Returns up to 365 days of audit log entries. Each entry includes a SHA-256 content hash for tamper detection. Response includes a Content-Disposition: attachment header.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes |
Response:
{
"exported_at": "2026-04-15T12:00:00Z",
"workspace_id": "uuid",
"total_entries": 4820,
"entries": [
{
"id": "uuid",
"workspace_id": "uuid",
"user_id": "uuid",
"action": "string",
"actor_email": "string",
"target_type": "string",
"target_id": "uuid",
"details": {},
"content_hash": "sha256-hex",
"created_at": "2026-04-15T12:00:00Z"
}
]
}
Additional error conditions:
| Code | Condition |
|---|---|
403 |
Workspace is not on the Enterprise plan |
POST /api/export-workspace¶
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | |
format |
string | Yes | "json", "csv", or "xlsx" |
includeAuditLog |
boolean | No | Include audit log entries. Default false. |
includeFull |
boolean | No | Include all raw entity metadata. Default false. |
CSV and XLSX responses are returned with the appropriate Content-Type and Content-Disposition: attachment headers.
Response (JSON format):
{
"exported_at": "2026-04-15T12:00:00Z",
"workspace_id": "uuid",
"format": "json",
"data": {
"identities": [],
"applications": [],
"devices": [],
"findings": [],
"audit_log": []
},
"entity_counts": {
"identities": 120,
"applications": 34,
"devices": 88,
"findings": 86
},
"truncated": false
}
If the export exceeds size limits, truncated is true and a cursor field is included for pagination.
Billing¶
| Method | Path | Description | Min role |
|---|---|---|---|
POST |
/api/create-checkout |
Create a Stripe checkout session for plan upgrade | Admin |
POST |
/api/create-portal-session |
Open the Stripe customer portal for billing management | Admin |
POST /api/create-checkout¶
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes | |
email |
string | Yes | Billing email address (RFC 5321) |
interval |
string | No | "monthly" or "annual". Default "monthly". |
Response:
{
"url": "https://checkout.stripe.com/...",
"sessionId": "string",
"interval": "monthly",
"priceId": "string",
"expiresAt": "2026-04-16T12:00:00Z"
}
Redirect the user to url to complete checkout. Sessions expire after 24 hours.
Additional error conditions:
| Code | Condition |
|---|---|
409 |
Workspace already has an active subscription at the requested interval |
POST /api/create-portal-session¶
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
UUID | Yes |
Response:
{
"url": "https://billing.stripe.com/...",
"expiresAt": "2026-04-16T12:00:00Z"
}
Redirect the user to url to manage their subscription. Sessions expire after 24 hours.
Additional error conditions:
| Code | Condition |
|---|---|
404 |
No Stripe customer record found for this workspace |
500 |
Stripe API error |
Request Format¶
All POST endpoints accept JSON request bodies with Content-Type: application/json. Every request must include a workspaceId field:
{
"workspaceId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
...
}
The backend verifies that the authenticated user is a member of the specified workspace before processing any query. workspaceId must be a valid UUID — malformed values return 400.
Error Handling¶
API errors return standard HTTP status codes:
| Code | Meaning |
|---|---|
400 |
Invalid request body, missing required fields, or malformed values |
401 |
Missing or invalid session token or API key |
403 |
Insufficient role permissions, IP not allowlisted, workspace mismatch, or trial expired |
404 |
Resource not found within workspace scope |
409 |
Conflict — e.g. analysis in progress, duplicate subscription, sync already running |
422 |
Validation failed against external provider (credential check) |
429 |
Rate limit exceeded |
500 |
Internal server error or upstream API error |
503 |
Upstream service unavailable (e.g. Anthropic API) |
Error responses are sanitized — no stack traces, internal paths, or credential fragments are included.
Rate Limiting¶
| Endpoint | Limit | Window |
|---|---|---|
/api/analyze |
1 run | 60 seconds per workspace |
/api/ai-chat (Free) |
25 messages | Per day |
/api/ai-chat (Pro) |
100 messages | Per day |
/api/ai-chat (Enterprise) |
Unlimited | — |
/api/api-keys (POST) |
10 active keys | Per workspace |
| MCP endpoints | 1,000 requests | Per hour per API key |
Security Headers¶
All API responses include the following security headers:
| Header | Value |
|---|---|
X-Frame-Options |
DENY |
X-Content-Type-Options |
nosniff |
Referrer-Policy |
strict-origin-when-cross-origin |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
For information on connecting platforms and what data each integration syncs, see Integrations Guide.