Skip to content

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 user
  • withManager — Admin or Manager role
  • withAdmin — 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:

json
{ "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:

json
{ "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:

json
{ "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:

json
{ "enabled": true }

POST /auth/oidc

Initiate an OIDC login flow. Returns the provider's authorization URL. Public endpoint.

Body:

json
{ "returnTo": "/dashboard" }

returnTo is optional (defaults to /dashboard). Must be a relative path.

Response:

json
{ "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:

  1. Validates the state parameter against KV (prevents CSRF)
  2. Exchanges the authorization code for tokens at the provider's token endpoint
  3. Verifies the ID token cryptographically via the provider's JWKS
  4. Finds existing user by OIDC subject or email, or auto-provisions if enabled
  5. Sets session cookie and redirects to the returnTo path

Errors: Redirects to /login?error=... on any failure.


GET /auth/me

Get the current authenticated user.

Response:

json
{ "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:

json
{ "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:

json
{ "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:

json
{ "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:

json
{ "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:

json
{ "needsSetup": true }

POST /setup

Complete initial setup. Only works if no users exist.

Body:

json
{
  "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:

json
[{ "id": "...", "name": "...", "status": "RUNNING", "model": "minimax/minimax-m2.5", ... }]

POST /agents

Create a new agent. Requires Manager+.

Body:

json
{
  "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:

FieldTypeDescription
namestringAgent name
descriptionstringAgent description
modelstringLLM model identifier
systemPromptstringSystem prompt
allowedCallTargetsstring[]Agent IDs this agent can call (empty = none)
heartbeatEnabledbooleanEnable periodic background runs
heartbeatIntervalMinutesintegerHeartbeat interval (5–1440 minutes)
heartbeatPromptstringMessage sent to the agent each heartbeat
workspaceMaxSizeMBintegerMax 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:

json
{
  "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:

json
{ "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:

json
{ "userId": "..." }

DELETE /agents/:id/access

Revoke access. Requires Manager+.

Body:

json
{ "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:

json
{
  "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:

json
{
  "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 @BotFather
  • webhookSecret — Optional secret for webhook verification
  • allowedChatIds — 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:

json
{ "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:

json
{ "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:

json
{ "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:

json
{
  "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:

json
[{ "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:

  1. Validates the state parameter against KV
  2. Exchanges the authorization code for access and refresh tokens
  3. Stores the tokens encrypted in the integration's credentials
  4. Sets the integration status to ACTIVE
  5. 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:

ValueMeaning
not_requiredIntegration doesn't use OAuth
pendingOAuth client credentials are saved but the authorization flow hasn't been completed
authorizedOAuth 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:

json
{
  "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:

json
{ "userId": "...", "role": "MANAGER" }
json
{ "userId": "...", "resetPassword": "new-password" }
json
{ "userId": "...", "resetMfa": true }
json
{ "userId": "...", "unlockAccount": true }

Invites

GET /invites

List invite links. Requires Admin.


POST /invites

Create an invite link. Requires Admin.

Body:

json
{ "role": "USER", "maxUses": 10, "expiresInHours": 72 }

Response:

json
{ "id": "...", "token": "abc123", "url": "https://prometheal.example.com/signup?invite=abc123", ... }

DELETE /invites

Revoke an invite link. Requires Admin.

Body:

json
{ "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:

json
{ "agentId": "...", "monthlyLimitCents": 1000 }

DELETE /usage/limits

Remove a spending limit. Requires Admin.

Body:

json
{ "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:

json
{"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:

json
{
  "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:

json
{
  "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:

json
{ "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:

json
{ "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/json

Body: 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:

json
{
  "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:

json
{
  "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 message events (ignores bot messages, edits, subtypes)
  • Matches incoming events to a channel by verifying the signature against each enabled channel's signing secret
  • If channelIds is configured, only messages from those Slack channels are processed
  • Returns 200 immediately; agent response is sent asynchronously via chat.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/events

Subscribe 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 sendMessage API
  • Splits responses longer than 4096 characters (Telegram limit) at newline boundaries

Setup: Register the webhook with Telegram:

bash
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.internal and 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

Released under the MIT License.