API Reference
All Prometheal REST endpoints. Authentication is via session cookie (browser) or agent token (LLM proxy).
Base URL: {NEXT_PUBLIC_APP_URL}/api
Auth helpers used in route handlers:
withAuth— Any authenticated userwithManager— Admin or Manager rolewithAdmin— Admin only
Authentication
POST /auth/login
Login with email and password. Rate limited: 10 requests per 15 minutes per IP. Per-account lockout after 5 failed attempts (15 minutes).
Body:
{ "email": "user@example.com", "password": "secret", "totpCode": "123456" }totpCode is only required when the account has MFA enabled. Omit it on the first attempt — if MFA is required, the response will indicate so.
Response: 200 with user object. Sets prometheal-session httpOnly cookie.
If MFA is enabled and totpCode is not provided:
{ "mfaRequired": true }Re-submit the request with the totpCode field.
Errors: 401 invalid credentials or invalid TOTP code, 423 account locked, 429 rate limited.
POST /auth/signup
Register a new account. Rate limited: 5 requests per hour per IP.
Body:
{ "email": "user@example.com", "password": "secret", "name": "User", "inviteToken": "abc123" }inviteToken is optional if open registration is enabled.
Response: 201 with user object. Sets session cookie.
Errors: 400 validation error, 403 registration disabled / invite invalid, 429 rate limited.
POST /auth/logout
Clear the session cookie.
Response: 200
GET /auth/oidc
Check if OIDC login is enabled. Public endpoint (no auth required).
Response:
{ "enabled": true }POST /auth/oidc
Initiate an OIDC login flow. Returns the provider's authorization URL. Public endpoint.
Body:
{ "returnTo": "/dashboard" }returnTo is optional (defaults to /dashboard). Must be a relative path.
Response:
{ "url": "https://idp.example.com/authorize?client_id=...&state=..." }The client should redirect the browser to the returned URL.
Errors: 400 OIDC not configured, 502 provider discovery failed.
GET /auth/oidc/callback
OIDC callback endpoint. Called by the OIDC provider after user authorization — not called directly by the client.
Query params: code (authorization code), state (CSRF token), error / error_description (if authorization failed)
Behavior:
- Validates the state parameter against KV (prevents CSRF)
- Exchanges the authorization code for tokens at the provider's token endpoint
- Verifies the ID token cryptographically via the provider's JWKS
- Finds existing user by OIDC subject or email, or auto-provisions if enabled
- Sets session cookie and redirects to the
returnTopath
Errors: Redirects to /login?error=... on any failure.
GET /auth/me
Get the current authenticated user.
Response:
{ "user": { "id": "...", "email": "...", "name": "..." }, "role": "ADMIN", "mfaEnabled": true }Errors: 401 not authenticated.
GET /auth/mfa/status
Check if MFA is enabled for the current user.
Response:
{ "mfaEnabled": true }POST /auth/mfa/setup
Generate a TOTP secret and QR code. The secret is stored (encrypted) but MFA is not yet enabled — the user must verify a code first.
Response:
{ "qrCode": "data:image/png;base64,...", "secret": "JBSWY3DPEHPK3PXP", "uri": "otpauth://totp/..." }Errors: 400 MFA already enabled, 401 not authenticated.
POST /auth/mfa/verify
Verify a TOTP code to enable MFA. Must call /auth/mfa/setup first.
Body:
{ "code": "123456" }Response: 200 { "success": true }
Errors: 400 invalid code / MFA already enabled / setup not started, 401 not authenticated.
POST /auth/mfa/disable
Disable MFA. Requires password confirmation.
Body:
{ "password": "your-password" }Response: 200 { "success": true }
Errors: 400 MFA not enabled, 401 invalid password / not authenticated.
Setup
GET /setup
Check if initial setup is needed.
Response:
{ "needsSetup": true }POST /setup
Complete initial setup. Only works if no users exist.
Body:
{
"instanceName": "My Prometheal",
"email": "admin@example.com",
"password": "secret",
"name": "Admin",
"openrouterApiKey": "sk-or-...",
"anthropicApiKey": "",
"openaiApiKey": ""
}Response: 201. Creates admin user and settings singleton.
Agents
GET /agents
List agents the current user can access.
Response:
[{ "id": "...", "name": "...", "status": "RUNNING", "model": "minimax/minimax-m2.5", ... }]POST /agents
Create a new agent. Requires Manager+.
Body:
{
"name": "My Agent",
"description": "Does things",
"model": "minimax/minimax-m2.5",
"systemPrompt": "You are a helpful assistant."
}Response: 201 with agent object.
GET /agents/:id
Get agent details including integrations.
Response: Agent object with integrations array.
PATCH /agents/:id
Update agent configuration. Requires Manager+.
Body: Partial agent fields:
| Field | Type | Description |
|---|---|---|
name | string | Agent name |
description | string | Agent description |
model | string | LLM model identifier |
systemPrompt | string | System prompt |
allowedCallTargets | string[] | Agent IDs this agent can call (empty = none) |
heartbeatEnabled | boolean | Enable periodic background runs |
heartbeatIntervalMinutes | integer | Heartbeat interval (5–1440 minutes) |
heartbeatPrompt | string | Message sent to the agent each heartbeat |
workspaceMaxSizeMB | integer | Max workspace disk usage in MB (0 = unlimited, default 3072) |
Response: Updated agent object. Includes lastHeartbeatAt timestamp when heartbeat is enabled.
GET /agents/:id/integrations
List integrations bound to this agent.
Response: Array of AgentIntegration objects with integration details.
POST /agents/:id/integrations
Bind an integration to this agent. Requires Manager+.
Body:
{
"integrationId": "...",
"enabled": true,
"allowedTools": ["tool1", "tool2"],
"blockedTools": [],
"confirmTools": ["dangerous_tool"],
"readOnly": false
}allowedTools, blockedTools, confirmTools are optional arrays.
GET /agents/:id/documents
List documents uploaded to this agent.
POST /agents/:id/documents
Upload a document (multipart/form-data). Max 50 MB. Synced to sandbox /documents/.
Allowed types: PDF, text, CSV, Markdown, HTML, JSON, XML, Office formats, images.
DELETE /agents/:id/documents/:docId
Delete an agent document.
GET /agents/:id/library
List library documents bound to this agent.
POST /agents/:id/library
Bind a library document to this agent.
Body:
{ "libraryDocumentId": "..." }DELETE /agents/:id/library/:docId
Unbind a library document from this agent.
GET /agents/:id/access
List users with access to this agent.
POST /agents/:id/access
Grant a user access to this agent. Requires Manager+.
Body:
{ "userId": "..." }DELETE /agents/:id/access
Revoke access. Requires Manager+.
Body:
{ "userId": "..." }GET /agents/:id/channels
List configured channels (Slack, Telegram) for this agent. Sensitive config fields (tokens, secrets) are masked as "***configured***". Requires Manager+.
Response:
{
"channels": [
{
"id": "...",
"name": "Support Slack",
"agentId": "...",
"type": "slack",
"enabled": true,
"config": { "botToken": "***configured***", "signingSecret": "***configured***", "channelIds": ["C123"] }
}
]
}POST /agents/:id/channels
Create or update a channel for this agent (upserts by agent + type). Requires Manager+. Sensitive config fields containing "token" or "secret" are automatically encrypted. Existing encrypted values are preserved if the field is omitted.
Body:
{
"type": "slack",
"enabled": true,
"config": {
"botToken": "xoxb-...",
"signingSecret": "abc123",
"channelIds": ["C01ABC123"]
}
}type must be "slack" or "telegram".
Slack config fields:
botToken— OAuth Bot Token (xoxb-...)signingSecret— App signing secret (for webhook verification)channelIds— Optional array of allowed Slack channel IDs
Telegram config fields:
botToken— Bot token from @BotFatherwebhookSecret— Optional secret for webhook verificationallowedChatIds— Optional array of allowed Telegram chat IDs
Response: 201 with channel object.
DELETE /agents/:id/channels
Delete a channel from this agent. Requires Manager+.
Query params: type (required) — "slack" or "telegram"
Response: 200
Chat
GET /chat/:agentId/conversations
List conversations for an agent.
POST /chat/:agentId/conversations
Create a new conversation.
Body:
{ "title": "New chat" }GET /chat/:agentId/conversations/:convId
Get a conversation with message history.
DELETE /chat/:agentId/conversations/:convId
Delete a conversation and all its messages.
POST /chat/:agentId/conversations/:convId/messages
Send a message and run an agent turn. Rate limited: 30 requests per 60 seconds per user.
Body:
{ "content": "Hello, please list the files in /home", "attachments": [] }Response: Server-sent event stream with events:
data: {"type":"text","content":"I'll list the files..."}
data: {"type":"tool_start","tool":"sandbox__shell","args":{"command":"ls -la /home"}}
data: {"type":"tool_end","tool":"sandbox__shell","result":"..."}
data: {"type":"text","content":"Here are the files..."}
data: {"type":"done","tokensIn":150,"tokensOut":200,"cost":0.001}Errors: 402 spending limit exceeded, 429 rate limited.
POST /chat/:agentId/conversations/:convId/confirm-tool
Approve or deny a pending tool confirmation.
Body:
{ "confirmationId": "...", "approved": true }GET /chat/:agentId/files/:fileId
Download a file from the agent's sandbox or documents.
Integrations
GET /integrations
List all integrations. Requires Admin.
POST /integrations
Create an integration. Requires Admin. Credentials are encrypted before storage.
Body:
{
"name": "GitHub",
"type": "github",
"transportType": "stdio",
"serverCommand": "npx -y @modelcontextprotocol/server-github",
"credentials": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." }
}PATCH /integrations/:id
Update an integration. Requires Admin.
DELETE /integrations/:id
Delete an integration and all agent bindings. Requires Admin.
GET /integrations/:id/tools
List tools available from this integration's MCP server. Triggers a connection if not already connected.
Response:
[{ "name": "search_repositories", "description": "...", "inputSchema": {...} }]GET /integrations/:id/oauth/authorize
Start an OAuth flow for an integration that requires it (e.g., Google Workspace). Requires Admin.
Redirects the browser to the provider's consent screen (e.g., Google). After the user grants access, the provider redirects back to the callback URL.
A CSRF state parameter is stored in KV with a 10-minute TTL.
GET /integrations/:id/oauth/callback
OAuth callback endpoint. Called by the OAuth provider after user authorization.
Query params: code (authorization code), state (CSRF token), error (if authorization failed)
Behavior:
- Validates the state parameter against KV
- Exchanges the authorization code for access and refresh tokens
- Stores the tokens encrypted in the integration's credentials
- Sets the integration status to ACTIVE
- Redirects to
/admin/integrations?oauth=success
Errors: Redirects to /admin/integrations?oauth=error&message=... on failure.
OAuth status
The GET /integrations and GET /integrations/:id responses include an oauthStatus field for integrations that use OAuth:
| Value | Meaning |
|---|---|
not_required | Integration doesn't use OAuth |
pending | OAuth client credentials are saved but the authorization flow hasn't been completed |
authorized | OAuth tokens are stored and the integration is ready to use |
Library
GET /library
List shared library documents. Requires Admin.
POST /library
Upload a document to the shared library (multipart/form-data). Requires Admin.
GET /library/:id
Get a library document's metadata.
DELETE /library/:id
Delete a library document and all agent bindings. Requires Admin.
Settings
GET /settings
Get instance settings. Requires Admin. API keys are masked as "configured" or "".
PATCH /settings
Update settings. Requires Admin. New API keys are encrypted before storage.
Body: Partial settings fields (instanceName, registrationEnabled, openrouterApiKey, oidcEnabled, oidcIssuerUrl, oidcClientId, oidcClientSecret, oidcAutoProvision, etc.)
GET /settings/system
System information (no auth required for health checks).
Response:
{
"node": "v22.0.0",
"platform": "linux",
"uptime": 86400,
"database": { "status": "connected", "latencyMs": 2 },
"sandbox": { "provider": "docker", "gvisor": true },
"counts": { "agents": 5, "running": 2, "users": 3, "integrations": 4 }
}Users
GET /users
List all users. Requires Admin.
PATCH /users
Update a user's role, reset password, reset MFA, or unlock account. Requires Admin.
Body:
{ "userId": "...", "role": "MANAGER" }{ "userId": "...", "resetPassword": "new-password" }{ "userId": "...", "resetMfa": true }{ "userId": "...", "unlockAccount": true }Invites
GET /invites
List invite links. Requires Admin.
POST /invites
Create an invite link. Requires Admin.
Body:
{ "role": "USER", "maxUses": 10, "expiresInHours": 72 }Response:
{ "id": "...", "token": "abc123", "url": "https://prometheal.example.com/signup?invite=abc123", ... }DELETE /invites
Revoke an invite link. Requires Admin.
Body:
{ "id": "..." }Usage & Spending
GET /usage
Get usage records. Filterable by agent ID and date range.
GET /usage/limits
Get spending limits for all agents.
PUT /usage/limits
Set a spending limit for an agent. Requires Admin.
Body:
{ "agentId": "...", "monthlyLimitCents": 1000 }DELETE /usage/limits
Remove a spending limit. Requires Admin.
Body:
{ "agentId": "..." }Audit
GET /audit
Paginated audit logs. Requires Admin.
Query params: page, limit, agentId, userId, eventType, from, to, search
GET /audit/export
Export audit logs as CSV. Requires Admin. Same filters as GET /audit.
GET /audit/export-jsonl
Export audit logs as JSONL or CEF for SIEM ingestion. Requires Admin.
Query params: agentId, eventType, from, to, format (json | cef), limit (default 10000, max 50000)
Response (JSONL): application/x-ndjson — one JSON object per line:
{"id":"...","timestamp":"...","eventType":"user.login","eventData":{...},"agentId":"...","agentName":"...","userId":"...","userName":"...","userEmail":"...","source":"prometheal"}Response (CEF): text/plain — one CEF line per event:
CEF:0|Prometheal|Prometheal|1.0|user.login|user.login|3|rt=... cs2=... cs2Label=userId msg=...POST /settings/siem-test
Test SIEM connectivity by sending a test event. Requires Admin.
Body:
{
"transport": "webhook",
"endpoint": "https://siem.example.com/events",
"format": "json",
"authHeader": "Bearer your-token"
}Response: 200 with { "success": true } or 502 with { "success": false, "error": "..." }.
Data Flow
GET /data-flow
Data movement logs. Requires Admin.
Query params: agentId, conversationId, direction (INBOUND/OUTBOUND/RESPONSE), from, to
Sandbox
GET /sandbox/:agentId/status
Get sandbox status, workspace usage, and quota.
Response:
{
"status": "running",
"sandboxId": "container-id",
"workspaceUsedMB": 150,
"workspaceMaxSizeMB": 3072
}workspaceUsedMB is null when the sandbox is not running.
POST /sandbox/:agentId/status
Control the sandbox. Requires Manager+.
Body:
{ "action": "start" }Actions: start, stop, restart, reset_workspace
reset_workspace stops the sandbox, deletes the Docker volume (all files), and restarts with a fresh workspace. This is irreversible.
GET /sandbox/:agentId/novnc
Get the noVNC desktop stream URL for an agent's sandbox.
Response:
{ "url": "http://container-ip:8080/vnc.html" }GET /sandbox/:agentId/warm
Pre-warm a sandbox (start it without sending a message).
GET /sandbox/health
Sandbox provider health check.
LLM Proxy
POST /llm-proxy/v1/chat/completions
OpenAI-compatible chat completions endpoint. For use by tools or scripts that need LLM access.
Headers:
Authorization: Bearer <agentToken>
x-prometheal-agent-id: <agentId>
Content-Type: application/jsonBody: Standard OpenAI chat completions request.
Response: Standard OpenAI chat completions response.
GET /llm-proxy/health
Health check for the LLM proxy.
Channels
Channels let external platforms (Slack, Telegram) route messages to agents. Channels can be created independently of agents and assigned later.
Admin API
GET /admin/channels
List all channels across all agents. Requires Admin. Sensitive config fields are masked.
Response:
{
"channels": [
{
"id": "...",
"name": "Support Slack",
"agentId": "..." | null,
"type": "slack",
"enabled": true,
"config": { "botToken": "***configured***", "signingSecret": "***configured***", "channelIds": [] },
"agent": { "id": "...", "name": "Support Agent" } | null
}
]
}POST /admin/channels
Create a channel. Requires Admin. Agent assignment is optional — unassigned channels store config but don't process messages.
Body:
{
"name": "My Slack Bot",
"agentId": "..." | null,
"type": "slack",
"enabled": true,
"config": {
"botToken": "xoxb-...",
"signingSecret": "abc123",
"channelIds": ["C01ABC123"]
}
}Response: 201 with channel object.
GET /admin/channels/:id
Get a single channel. Requires Admin.
PATCH /admin/channels/:id
Update a channel. Requires Admin. Omitted config fields preserve existing encrypted values.
Body: Partial fields — name, agentId (set to null to unassign), enabled, config.
Response: Updated channel object.
DELETE /admin/channels/:id
Delete a channel. Requires Admin.
Response: 200
Webhooks (Public)
These endpoints receive incoming messages from external platforms. They are public (no auth required) — verification is done via platform-specific signatures.
POST /channels/slack/events
Slack Events API webhook endpoint.
Verification: HMAC-SHA256 signature via x-slack-signature and x-slack-request-timestamp headers, validated against the channel's signingSecret. Requests older than 5 minutes are rejected.
Behavior:
- Responds to Slack URL verification challenges (
{"type": "url_verification"}) - Handles
messageevents (ignores bot messages, edits, subtypes) - Matches incoming events to a channel by verifying the signature against each enabled channel's signing secret
- If
channelIdsis configured, only messages from those Slack channels are processed - Returns
200immediately; agent response is sent asynchronously viachat.postMessage
Setup: Add the webhook URL as the Request URL in your Slack app's Event Subscriptions:
https://your-prometheal.example.com/api/channels/slack/eventsSubscribe to the message.channels and message.im bot events.
POST /channels/telegram/webhook
Telegram Bot webhook endpoint.
Verification: Checks X-Telegram-Bot-Api-Secret-Token header against the channel's webhookSecret (if configured).
Behavior:
- Only handles text messages (ignores media, commands, etc.)
- Matches incoming updates to a channel by webhook secret and allowed chat IDs
- Sends typing indicator, then responds via
sendMessageAPI - Splits responses longer than 4096 characters (Telegram limit) at newline boundaries
Setup: Register the webhook with Telegram:
curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
-d "url=https://your-prometheal.example.com/api/channels/telegram/webhook" \
-d "secret_token=<WEBHOOK_SECRET>"Channel Architecture
- Channel users: Each external user (Slack user, Telegram user) gets a synthetic Prometheal user with email
{type}-{userId}@channel.internaland no login access - Conversations: Keyed by
channel:{type}:{externalChatId}— messages from the same chat reuse the same conversation - History: Last 20 messages are included as context for each agent turn
- Unassigned channels: Channels with no agent are stored but webhooks skip them