Otto Docs
Reference

API Endpoints Reference

> Complete reference for the Otto backend REST API. > > Base URL: `http://localhost:8000` (configurable via `BACKEND_API_URL`) > > Framework: FastAPI (Python)

Complete reference for the Otto backend REST API.

Base URL: http://localhost:8000 (configurable via BACKEND_API_URL)

Framework: FastAPI (Python)

Authentication

Otto supports two authentication modes controlled by DEPLOYMENT_MODE:

ModeHeaderBehavior
self-hosted (default)NoneAll requests pass through without authentication
hostedAuthorization: Bearer <firebase-id-token>Firebase ID token validated on every request

Auth-exempt paths (hosted mode): /health, /webhook/slack, /docs, /openapi.json, and CORS preflight (OPTIONS).

Slack endpoints use HMAC signature verification via SLACK_SIGNING_SECRET, independent of Firebase auth.


Health and System

No route prefix. Registered in app/routers/health.py.

GET /

Root welcome endpoint.

  • Auth: None
  • Response:
    {"message": "Welcome to Otto AI Assistant"}

GET /health

Health check for containers, App Engine, and the control plane. Returns operational metadata only (never customer data).

  • Auth: None (exempt)
  • Response:
    {
      "status": "healthy",
      "version": "1.2.0",
      "users": 5,
      "tasks_24h": 42
    }

GET /api/health

Detailed API health check.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "status": "healthy",
      "timestamp": "2026-02-19T12:00:00Z",
      "version": "1.0.0",
      "service": "otto-backend",
      "deployment_type": "cloud_frontend"
    }

GET /api/version

API version and feature information.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "version": "1.0.0",
      "features": ["skills", "subagents", "mcp"],
      "deployment_type": "cloud_frontend",
      "api_docs": "/docs",
      "redis_type": "standard",
      "database_type": "sqlite"
    }

GET /system/info

System info -- verifies Node.js/npm/npx availability for MCP servers.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "python_version": "3.12.0",
      "node_available": true,
      "npm_available": true,
      "npx_available": true,
      "node_version": "22.0.0",
      "npm_version": "10.0.0"
    }

Tasks

No route prefix. Registered in app/routers/tasks.py.

GET /tasks

Get all tasks with summary information.

  • Auth: Yes (hosted mode)
  • Response: List[TaskResponse]

GET /task/{task_id}

Get a specific task by ID.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    task_idstringTask ID
  • Response: TaskResponse

GET /task/{task_id}/subtasks

Get all subtasks for a parent task.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    task_idstringParent task ID
  • Response: List[TaskResponse]

DELETE /task/{task_id}

Delete or archive a task. First call archives; second call deletes permanently.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    task_idstringTask ID
  • Response: 204 No Content

GET /task/{task_id}/worker-status

Get task worker status for monitoring.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    task_idstringTask ID
  • Response:

    {
      "task_id": "abc123",
      "database_status": "running"
    }

POST /chat

Create a new task from user input. Queues agent processing via ARQ.

  • Auth: Yes (hosted mode)
  • Request body:
    {
      "human_input": "Research competitor pricing",
      "creator_id": "user-123"
    }
  • Response:
    {"task_id": "task-456"}

POST /human_response

Resume an agent task that is waiting for human input.

  • Auth: Yes (hosted mode)
  • Request body:
    {
      "task_id": "task-456",
      "human_response": "Yes, proceed with option A",
      "user_id": "user-123"
    }
  • Response:
    {"status": "resumed"}

GET /worker/health

Check if ARQ workers are running.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "status": "healthy",
      "job_enqueued": true
    }

TaskResponse Model

FieldTypeDescription
idstringTask ID
titlestringTask title
descriptionstringTask description
statusstringpending, running, completed, failed, waiting_input, waiting_subagents
created_atstringISO 8601 timestamp
updated_atstringISO 8601 timestamp
progressinteger0-100
questionstringPending question (when status=waiting_input)
planstringAgent's execution plan
last_tool_call_idstringTool call ID for resume (may be JSON array for subagent dispatches)
checkpoint_thread_idstringRedis checkpoint thread identifier
logsarrayList of TaskLogEntry objects
outputsarrayList of TaskOutput objects
creatorstringCreator user ID
assigneestringAssigned user ID
webhook_typestringSource webhook type (if created via webhook)
webhook_payloadobjectOriginal webhook payload
webhook_headersobjectOriginal webhook headers
source_ipstringSource IP address
celery_task_idstringARQ job ID (legacy field name)
parent_task_idstringParent task ID (for subtasks)
task_typestringtask or subtask
agent_typestringSubagent type: general, research, tool-specialist
token_budgetintegerToken budget for subagents
tokens_usedintegerTokens consumed

Users

Route prefix: /users. Registered in app/routers/users.py.

GET /users

Get all users in the system.

  • Auth: Yes (hosted mode)
  • Response: List[UserDict]

PUT /users/{user_id}

Update user information. Fields provided are marked as manual overrides and will not be overwritten by Slack sync.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    user_idstringUser ID
  • Request body (all fields optional):

    {
      "name": "Jane Doe",
      "email_address": "jane@example.com",
      "role": "Engineering Lead",
      "preferences": {"notification_channel": "slack"}
    }
  • Response: Updated user dict

DELETE /users/{user_id}/overrides

Clear all manual overrides for a user, allowing Slack sync to update all fields again.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    user_idstringUser ID
  • Response:

    {"status": "success", "message": "Manual overrides cleared"}

GET /users/{user_id}/notifications

Get notifications for a specific user.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    user_idstringUser ID
  • Response: List[NotificationDict]

POST /users/{user_id}/notifications/{notification_id}/mark_read

Mark a notification as read.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    user_idstringUser ID
    notification_idstringNotification ID
  • Response:

    {"status": "success", "message": "Notification marked as read"}

DELETE /users/{user_id}/notifications/{notification_id}

Delete a notification.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    user_idstringUser ID
    notification_idstringNotification ID
  • Response: 204 No Content


Files

No route prefix. Registered in app/routers/files.py.

POST /upload-file

Upload a file for a user. Validates file extension, checks storage quota, and saves to user-specific sandbox directory.

  • Auth: Yes (hosted mode)

  • Request: Multipart form data

    FieldTypeDescription
    user_idstringUser ID
    filefileFile to upload
  • Allowed extensions: .txt, .md, .json, .csv, .pdf, .docx, .pptx, .xlsx, .png, .jpg, .jpeg, .gif, .webp

  • Response:

    {
      "message": "File uploaded successfully",
      "filepath": "/tmp/otto-files/user-123/report.pdf"
    }

Slack Integration

Route prefix: /slack. Registered in app/routers/slack.py.

All Slack endpoints verify requests via HMAC signature using SLACK_SIGNING_SECRET. This is separate from Firebase auth.

POST /slack/interactive

Handle Slack interactive components (button clicks, e.g., "Stop Reminders").

  • Auth: Slack signature verification
  • Request: Form-encoded body with payload (JSON string from Slack)
  • Response: Slack-format JSON

POST /slack/dm_response

Main Slack Events API handler. Dispatches internally based on event type.

  • Auth: Slack signature verification
  • Request: JSON body (Slack Events API payload)
  • Response: JSON (always 200 for Slack)

Event types handled:

Event TypeBehavior
url_verificationResponds with challenge token
message (DM or channel)Routes to human response for waiting tasks, or creates continuation tasks for thread replies
app_mentionCreates new tasks. Recognizes schedule, remind, and recurring remind commands via pattern matching
member_joined_channelSyncs channel, triggers project onboarding (if Otto was invited), or adds user membership
member_left_channelRemoves user membership
channel_deletedDeactivates channel in database

GET /slack/health

Slack integration health check. Tests real Slack API connectivity.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "status": "healthy",
      "service": "otto-slack-integration",
      "slack_configured": true,
      "slack_connected": true
    }

Webhooks

Route prefix: /webhook. Registered in app/routers/webhooks.py.

POST /webhook/{webhook_type}

Receive and queue a webhook of any type. Parses JSON body and enqueues for agent processing.

  • Auth: None for /webhook/slack (exempt); Yes for others (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    webhook_typestringWebhook type identifier (e.g., github, stripe, slack)
  • Request body: Any JSON payload

  • Response:

    {
      "status": "received",
      "message_id": "msg-789",
      "webhook_type": "github"
    }

GET /webhook/health

Webhook subsystem health check.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "status": "healthy",
      "queue_stats": {},
      "service": "otto-webhook-ingestor"
    }

GET /webhook/stats

Webhook processing statistics.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "queue_stats": {},
      "recent_failed_messages": []
    }

GET /webhook/status

Webhook processing worker status.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "status": "running",
      "queue_stats": {}
    }
    status is one of: running, stopped, not_initialized.

MCP Status (Legacy)

No route prefix. Registered in app/routers/mcp.py.

GET /mcp/status

Get MCP status. MCP is now handled by ARQ workers, so this returns a static message.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "status": "handled_by_arq_workers",
      "message": "MCP tools are initialized per-worker",
      "servers": [],
      "tools": [],
      "note": "Use /api/mcp/status for configuration details"
    }

MCP Configuration

Route prefix: /api/mcp. Registered in app/api/mcp_endpoints.py.

GET /api/mcp/status

Get current MCP server status without changing configuration.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "initialized": true,
      "total_servers": 3,
      "successful_servers": 2,
      "failed_servers": 1,
      "available_tools": 15,
      "tool_names": ["tavily_search", "gamma_create", "..."],
      "servers": {
        "tavily": {"status": "connected", "tools": 2},
        "gamma": {"status": "error", "error": "..."}
      }
    }

POST /api/mcp/configure

Configure MCP servers. Reloads configuration with new server definitions.

  • Auth: Yes (hosted mode)
  • Request body:
    {
      "mcpServers": {
        "tavily": {
          "command": "npx",
          "args": ["-y", "tavily-mcp"],
          "env": {"TAVILY_API_KEY": "tvly-..."}
        }
      }
    }
  • Response: MCPConfigResponse with server counts and per-server status

DELETE /api/mcp/configure

Clear MCP configuration and shut down all servers.

  • Auth: Yes (hosted mode)
  • Response: MCPConfigResponse (all counts zero)

Skills

Route prefix: /api/skills. Registered in app/api/skills_endpoints.py.

GET /api/skills/

List all available skills.

  • Auth: Yes (hosted mode)
  • Response: List[SkillSummary]
    [
      {
        "name": "gamma-presentation",
        "description": "Create presentations using Gamma",
        "category": "productivity",
        "source": "built-in",
        "tags": ["gamma", "presentation"],
        "available": true,
        "missing_requirements": []
      }
    ]

GET /api/skills/requirements

Show all skill requirements and their status (binaries, env vars, Python/Node packages, platform).

  • Auth: Yes (hosted mode)
  • Response:
    {
      "total_skills": 12,
      "available": 10,
      "unavailable": 2,
      "skills": [{"name": "...", "requirements": {}, "status": "available"}]
    }

GET /api/skills/{skill_name}

Get detailed information for a specific skill.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    skill_namestringSkill name
  • Response: SkillDetail

    {
      "name": "gamma-presentation",
      "description": "...",
      "category": "productivity",
      "version": "1.0.0",
      "author": "otto",
      "allowed_tools": ["gamma_create", "gamma_status", "wait"],
      "tags": ["gamma", "presentation"],
      "source": "built-in",
      "content": "# Skill instructions...",
      "supporting_files": [],
      "available": true,
      "missing_requirements": []
    }

POST /api/skills/match

Match skills to a query using semantic similarity.

  • Auth: Yes (hosted mode)
  • Request body:
    {
      "query": "make a slide deck about AI trends",
      "threshold": 0.6,
      "max_skills": 3
    }
    threshold and max_skills are optional (defaults: 0.6 and 3).
  • Response:
    {
      "query": "make a slide deck about AI trends",
      "matched_skills": [{"name": "gamma-presentation", "...": "..."}]
    }

POST /api/skills/reload

Reload skills from the filesystem.

  • Auth: Yes (hosted mode)
  • Response:
    {"message": "Skills reloaded successfully", "count": 12}

GET /api/skills/debug/similarity/{query}

Get similarity scores for all skills against a query. For debugging and analytics.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    querystringSearch query (URL-encoded)
  • Response:

    {
      "query": "create presentation",
      "scores": [
        {"skill_name": "gamma-presentation", "score": 0.82},
        {"skill_name": "email-draft", "score": 0.15}
      ]
    }

POST /api/skills/

Create a new skill.

  • Auth: Yes (hosted mode)
  • Request body:
    {
      "name": "my-skill",
      "description": "Does something useful",
      "category": "productivity",
      "content": "# Skill instructions\n...",
      "tags": ["tag1", "tag2"],
      "author": "jane",
      "allowed_tools": ["read_file", "write_file"]
    }
    tags, author, and allowed_tools are optional.
  • Response: SkillDetail

PUT /api/skills/{skill_name}

Update an existing skill. All body fields are optional.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    skill_namestringSkill name
  • Request body:

    {
      "description": "Updated description",
      "content": "# Updated instructions\n..."
    }
  • Response: SkillDetail

DELETE /api/skills/{skill_name}

Delete a skill.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    skill_namestringSkill name
  • Response:

    {"message": "Skill 'my-skill' deleted successfully"}

POST /api/skills/generate

Generate a skill using AI from a natural language description.

  • Auth: Yes (hosted mode)
  • Request body:
    {"description": "A skill that creates Jira tickets from Slack messages"}
  • Response:
    {
      "name": "jira-ticket-creator",
      "description": "Creates Jira tickets from Slack messages",
      "category": "project-management",
      "content": "# Generated skill instructions...",
      "tags": ["jira", "slack", "tickets"]
    }

Channels

Route prefix: /api/channels. Registered in app/api/channels_endpoints.py.

GET /api/channels/

List all discovered Slack channels.

  • Auth: Yes (hosted mode)
  • Response: List[SlackChannelResponse]
    [
      {
        "id": "C01234",
        "name": "marketing",
        "team_id": "T01234",
        "discovered_at": "2026-02-19T12:00:00Z",
        "is_active": true,
        "member_count": 8,
        "unconfigured_count": 2
      }
    ]

GET /api/channels/{channel_id}

Get details for a specific channel.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    channel_idstringSlack channel ID
  • Response: Channel dict

POST /api/channels/sync

Force sync all channels from Slack.

  • Auth: Yes (hosted mode)
  • Response:
    {"message": "Sync complete", "stats": {"synced": 5, "new": 1}}

POST /api/channels/{channel_id}/sync

Sync a single channel from Slack.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    channel_idstringSlack channel ID
  • Response:

    {"message": "Channel synced", "result": {}}

GET /api/channels/{channel_id}/members

List all members of a channel with their roles.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    channel_idstringSlack channel ID
  • Response: List[ChannelMembershipResponse]

    [
      {
        "id": "mem-1",
        "user_id": "user-123",
        "user_name": "Jane Doe",
        "user_email": "jane@example.com",
        "slack_user_id": "U01234",
        "role": "Engineering Lead",
        "responsibilities": "Frontend development",
        "joined_at": "2026-01-15T09:00:00Z"
      }
    ]

PUT /api/channels/{channel_id}/members/{user_id}

Update role and responsibilities for a channel member.

  • Auth: Yes (hosted mode)

  • Path parameters:

    ParameterTypeDescription
    channel_idstringSlack channel ID
    user_idstringUser ID
  • Request body (all fields optional):

    {
      "role": "Project Manager",
      "responsibilities": "Sprint planning, stakeholder communication"
    }
  • Response:

    {"message": "Membership updated"}

Team Management

Route prefix: /team. Registered in app/routers/team.py.

Only active when DEPLOYMENT_MODE=hosted. Returns 404 in self-hosted mode.

POST /team/invite

Invite a new team member. Enforces tier-based user limits (MAX_USERS).

  • Auth: Yes (hosted mode)
  • Request body:
    {
      "email": "newuser@example.com",
      "name": "New User",
      "role": "member"
    }
    role is optional (default: member).
  • Response:
    {
      "id": "user-789",
      "name": "New User",
      "email": "newuser@example.com"
    }

GET /team/info

Return team size and tier information.

  • Auth: Yes (hosted mode)
  • Response:
    {
      "user_count": 5,
      "max_users": 10,
      "tier": "pro",
      "can_invite": true
    }

Standalone Services

These are separate FastAPI applications that run independently from the main API.

Webhook Ingestor (port 8001)

Standalone webhook receiver. Run via app/webhook/ingestor.py.

MethodPathDescription
POST/webhook/{webhook_type}Receive and queue webhooks
GET/healthHealth check
GET/statsWebhook processing statistics
GET/Service information

Slack Handler (port 8002)

Standalone Slack event handler. Run via app/slack/handler.py.

MethodPathDescription
POST/slack/commandHandle Slack /otto slash command
POST/slack/interactiveHandle Slack interactive components
GET/slack/healthHealth check

Auto-Generated Documentation

Available when DEPLOYMENT_MODE is not hosted:

PathDescription
/docsSwagger UI (interactive API documentation)
/redocReDoc API documentation
/openapi.jsonOpenAPI schema JSON

Endpoint Summary

Main Application (port 8000): 49 endpoints

CategoryCountPrefix
Health and System5/, /health, /api/health, /api/version, /system/info
Tasks8/tasks, /task/{id}, /chat, /human_response, /worker/health
Users6/users
Files1/upload-file
Slack Integration3/slack
Webhooks4/webhook
MCP Status (legacy)1/mcp/status
MCP Configuration3/api/mcp
Skills10/api/skills
Channels6/api/channels
Team Management2/team

Standalone Services: 7 endpoints

ServiceCountPort
Webhook Ingestor48001
Slack Handler38002

On this page

Authentication
Health and System
GET /
GET /health
GET /api/health
GET /api/version
GET /system/info
Tasks
GET /tasks
GET /task/{task_id}
GET /task/{task_id}/subtasks
DELETE /task/{task_id}
GET /task/{task_id}/worker-status
POST /chat
POST /human_response
GET /worker/health
TaskResponse Model
Users
GET /users
PUT /users/{user_id}
DELETE /users/{user_id}/overrides
GET /users/{user_id}/notifications
POST /users/{user_id}/notifications/{notification_id}/mark_read
DELETE /users/{user_id}/notifications/{notification_id}
Files
POST /upload-file
Slack Integration
POST /slack/interactive
POST /slack/dm_response
GET /slack/health
Webhooks
POST /webhook/{webhook_type}
GET /webhook/health
GET /webhook/stats
GET /webhook/status
MCP Status (Legacy)
GET /mcp/status
MCP Configuration
GET /api/mcp/status
POST /api/mcp/configure
DELETE /api/mcp/configure
Skills
GET /api/skills/
GET /api/skills/requirements
GET /api/skills/{skill_name}
POST /api/skills/match
POST /api/skills/reload
GET /api/skills/debug/similarity/{query}
POST /api/skills/
PUT /api/skills/{skill_name}
DELETE /api/skills/{skill_name}
POST /api/skills/generate
Channels
GET /api/channels/
GET /api/channels/{channel_id}
POST /api/channels/sync
POST /api/channels/{channel_id}/sync
GET /api/channels/{channel_id}/members
PUT /api/channels/{channel_id}/members/{user_id}
Team Management
POST /team/invite
GET /team/info
Standalone Services
Webhook Ingestor (port 8001)
Slack Handler (port 8002)
Auto-Generated Documentation
Endpoint Summary
Main Application (port 8000): 49 endpoints
Standalone Services: 7 endpoints