Skip to content

API Reference

This guide describes common routes. The generated HTTP catalog is the generated mounted-route inventory and links each route to its implementation. Most routes use /api; hosted artifacts, the gateway, and /v1 have separate paths. Authentication can use a session cookie or Bearer token. Public health/authentication routes and signed webhook/SSO flows have their own requirements.

For scripts, use an Authorization: Bearer <token> header. The browser normally uses an HttpOnly session cookie. Throughout these docs the placeholder is $OCTIPUS_API_TOKEN. There are two ways to get a token:

  • Login JWT — POST /api/auth/login-mobile returns a session token in the body for native/script clients. POST /api/auth/login sets the browser cookie and does not return the token in JSON. Sessions expire.
  • Personal Access Token — create a long-lived token under Settings → API Tokens in the web UI (or POST /api/auth/api-tokens). Best for scripts, cron, and webhooks. Manage them via the API Tokens endpoints; revoke with DELETE /api/auth/api-tokens/:id.

Admin-only endpoints (anything marked admin, plus system-scoped vault writes) require a token belonging to an admin user. Export it once:

Terminal window
export OCTIPUS_API_TOKEN="<your-token>"
curl -H "Authorization: Bearer $OCTIPUS_API_TOKEN" http://localhost:3005/api/auth/me
MethodEndpointAuthDescription
GET/api/healthNoBasic health check
GET/api/health/detailedNoService status with latencies
GET/api/health/readyNoReadiness probe
GET/api/health/liveNoLiveness probe
GET/api/health/storageNoStorage provider (cache/queue/pub-sub) health
MethodEndpointAuthDescription
POST/api/auth/registerNoRegister new user (audited as user_created); security.registration applies, inviteToken? is redeemed with the account (SPACES.md → Registration modes)
GET/api/auth/registrationNo{mode, firstAccount}: the registration mode
POST/api/auth/loginNoLogin with credentials; a TOTP account without totpCode gets 401 { requiresTOTP: true } (audited as login / login_failed)
POST/api/auth/login-mobileNoLogin returning bearer token in response body (for native clients)
POST/api/auth/logoutYesLogout and invalidate session
GET/api/auth/meYesGet current user info
GET/api/auth/ws-ticketYesGet a short-lived (60 s) token to sign a socket in: the gateway auth frame, or the /voice URL
POST/api/auth/passkey/register/optionsYesGenerate WebAuthn registration options
POST/api/auth/passkey/register/verifyYesVerify WebAuthn registration response
POST/api/auth/passkey/auth/optionsNoGenerate WebAuthn authentication options
POST/api/auth/passkey/auth/verifyNoVerify WebAuthn authentication response
POST/api/auth/totp/setupYesSetup TOTP 2FA
POST/api/auth/totp/enableYesEnable TOTP after verification
POST/api/auth/totp/disableYesDisable TOTP 2FA
POST/api/auth/linkYesRedeem channel link code

Login, login-mobile and register take an optional returnTo: a same-origin path (one leading /, no //, no backslash, no whitespace or control characters). Anything else is refused with 400; a valid one is echoed back as returnTo in the response (/ when none was sent).

MethodEndpointDescription
GET/api/agentsList all agents
POST/api/agentsSpawn new agent
GET/api/agents/:idGet agent details
DELETE/api/agents/:idStop and remove agent
POST/api/agents/:id/messageSend message to agent
GET/api/agents/:id/eventsGet agent events (cursor-based: ?after=<seq>)
MethodEndpointDescription
GET/api/sessionsList sessions
GET/api/sessions/:idGet session details
GET/api/sessions/:id/messagesGet session messages
MethodEndpointDescription
GET/api/verification/:sessionIdGet verification evidence and status for a session
MethodEndpointDescription
GET/api/modelsList all models
POST/api/modelsRegister new model
GET/api/models/:nameGet model details
PATCH/api/models/:nameUpdate model config
DELETE/api/models/:nameDelete model
POST/api/models/:name/defaultSet as default model
GET/api/topicsGet topic bindings and configuration
GET/api/models/healthCheck provider health
GET/api/models/cli/statusCLI tool availability
GET/api/models/cli/quotaCLI quota status
GET/api/models/providers/ollama/modelsList available Ollama models
GET/api/models/providers/litellm/modelsList LiteLLM models
GET/api/models/providers/:provider/knownKnown models for a provider

A user’s own model rows, run with their own key (SPACES.md → Own models). Owner-only, admins included; another user’s model answers 404. Bodies are validated against an allowlist (400 otherwise).

MethodEndpointDescription
GET/api/me/modelsMy models, with the allowed providers and the topics I may bind
POST/api/me/modelsAdd a model (201)
PATCH/api/me/models/:slugUpdate one of my models
DELETE/api/me/models/:slugDelete one of my models
MethodEndpointDescription
GET/api/rolesList all roles: tools, lane, description, prompt, critical rules, read-only flag
POST/api/rolesCreate a role (admin). Body: { role, systemPromptTemplate, toolIds, description?, coreToolIds?, criticalRules?, defaultTopic?, readOnly? }
PATCH/api/roles/:roleUpdate a role (admin). Every field optional; absent = unchanged
DELETE/api/roles/:roleDelete a user-created role (admin). A role that ships with Octipus returns 400

A role’s LANE is its defaultTopic resolved through canonicalTopic — the model binding it runs on. Nothing selects a role out of a lane; a role is chosen by an agent naming it in spawn_child, off its one-line description.

MethodEndpointDescription
GET/api/topicsList all topics with current model bindings and config
PATCH/api/topics/:topic/configUpdate topic config (executorModel, temperature, maxTokens) (admin)
PUT/api/topics/:topic/bindingSet topic’s primary/backup model binding (admin)
POST/api/topics/assign-allBind one model as primary for all text topics (admin)
MethodEndpointDescription
GET/api/hooksList all hooks
POST/api/hooksCreate hook
GET/api/hooks/:idGet hook details
PATCH/api/hooks/:idUpdate hook
DELETE/api/hooks/:idDelete hook
POST/api/hooks/:id/toggleToggle hook enabled state
MethodEndpointDescription
GET/api/vaultList credentials
POST/api/vaultStore credential
PATCH/api/vault/:idUpdate credential
DELETE/api/vault/:idDelete credential
POST/api/vault/:id/rotateRotate credential
MethodEndpointDescription
POST/api/pipelinesCreate and start a pipeline from a template. Returns 202 with { pipelineId, sessionId } as soon as the run has an id — the run itself continues in the background.
GET/api/pipelinesList user’s pipeline runs
GET/api/pipelines/:idGet pipeline detail with nodes
POST/api/pipelines/:id/stopStop a running pipeline
GET/api/pipelines/:id/planList the plan a foreach node iterates
POST/api/pipelines/:id/planAppend plan items (editable while running)
PATCH/api/pipelines/:id/plan/:itemIdEdit or reorder a plan item
DELETE/api/pipelines/:id/plan/:itemIdDrop a still-pending plan item
POST/api/pipelines/:id/pausePause at the next node boundary
POST/api/pipelines/:id/resumeResume from the newest checkpoint, or from fromSeq (rewind)
GET/api/pipelines/:id/checkpointsList checkpoints (node-boundary snapshots)
PATCH/api/pipelines/:id/checkpoints/:seqEdit what the next node will read
GET/api/pipelines/templatesList pipeline templates
POST/api/pipelines/templatesCreate pipeline template
PUT/api/pipelines/templates/:idUpdate pipeline template
DELETE/api/pipelines/templates/:idDelete pipeline template
MethodEndpointDescription
GET/api/runs/:sessionId/eventsThe run log — one ordered stream of node, plan and tool events
GET/api/runs/:sessionId/traceThe same log folded into spans, with per-span duration and cost
MethodEndpointDescription
GET/api/skillsList domain knowledge skills
GET/api/skills/:idGet skill details
POST/api/skillsCreate custom skill
PATCH/api/skills/:idUpdate skill
DELETE/api/skills/:idDelete custom skill

Per-user root agent persona — name, tone, narration volume, free-form self-facts — plus the per-arm voices that shadow it. Same controls as the /persona slash command (see CHAT-COMMANDS.md) and the web /persona page. Writes delegate to handlePersonaCommand so validation matches across all three surfaces.

MethodEndpointDescription
GET/api/personaResolved persona for the current user (name, pronouns, tone, narration, presetId, signaturePhrases, userFacts)
GET/api/persona/presetsList shipped persona presets (personas/*.yaml) — no auth
PATCH/api/personaUpdate one or more of { name, tone, narration, presetId }
POST/api/persona/factsAppend a free-form self-fact ({ fact }, 4–280 chars)
DELETE/api/persona/facts/:idxRemove the N-th free-form fact (0-indexed across extra:*)
POST/api/persona/resetRestore Octipus default — drops custom name and facts
GET/api/persona/armsPer-arm persona bindings ({ arms: { review: "terse-engineer" } }); {} when no arm has its own voice
PUT/api/persona/arms/:roleShadow one arm’s voice with { presetId }. 400 with the reason for an unknown role/preset; for the root identity use PATCH /api/persona
DELETE/api/persona/arms/:roleClear it — that arm runs with no persona, as before
MethodEndpointDescription
GET/api/toolsList registered tools
GET/api/tools/:idGet tool details
GET/api/tools/allAll built-in and expanded MCP tool definitions
GET/api/tools/permissionsUser permission overrides
PUT/api/tools/permissionsSet an override, optionally scoped with expiry
DELETE/api/tools/permissions/:toolId/:actionReset permission
POST/api/tools/:toolId/tools/:toolName/executeExecute a tool function

An override body contains toolId, action, level (ALLOW, ASK, DENY), optional reason, and optional scope. A scope requires a future expiresAt and at least one of sessionId, workspaceId, pathPattern, commandPattern. Scope conditions are combined; unavailable/mismatched scope evidence blocks. A scoped ALLOW cannot overwrite an existing DENY (409); review that denial first. Removing an override restores the manifest default, which may be ALLOW; set ASK explicitly when the intent is to require approval.

Direct execution accepts { "args": { ... } } and runs unattended. ASK returns HTTP 409 with code: "approval_required"; other execution failures return 400 with code: "tool_execution_failed". An attended agent’s descendants can use the session’s approval surface. MCP permissions use tool ID mcp, action <serverId>.<remoteToolName>. Argument patterns constrain authorization, not filesystem or shell isolation.

MethodEndpointDescription
GET/api/notificationsList notifications (paginated)
POST/api/notifications/:id/readMark notification as read
POST/api/notifications/read-allMark all notifications read
MethodEndpointDescription
POST/api/chatSend a chat message. Without a channel that can show an approval prompt, the turn is unattended: actions at ASK are refused, not asked. Interactive answers are replaced by a plain line unless the body declares renders: ["inline_ui"]; toolsUsed: true adds the tool names the turn called (the eval runner)
POST/api/chat/approveRespond to an approval request
MethodEndpointDescription
POST/api/readerFetch and extract article/page content
MethodEndpointDescription
POST/api/documents/uploadUpload one or more documents
GET/api/documentsList documents (with optional filtering by category/status)
GET/api/documents/:idGet document details
GET/api/documents/:id/rawStream original file (with ?download=1 for attachment mode)
DELETE/api/documents/:idDelete document
POST/api/documents/:id/cancelCancel document processing
MethodEndpointDescription
POST/api/researchStart a deep research job
GET/api/research/:jobIdGet persisted research job status and results
MethodEndpointDescription
GET/api/tasksList user’s tasks (?status=, ?due=today, ?category=, ?view=next for next-action order with a bucket and reason each)
GET/api/tasks/:idGet one task
POST/api/tasksCreate a task (parentId, blockedBy[], estimate optional)
PATCH/api/tasks/:idUpdate task; status is open / in_progress / done / archived; parentId: null and blockedBy: [] clear the links
DELETE/api/tasks/:idDelete task (its sub-tasks become top-level)
GET/api/tasks/role-agentsThe caller’s role agents: task counts, on/off, pipelineTemplate (the template each task runs through, or null)
PUT/api/tasks/role-agentsTurn a role agent on or off ({role, enabled}); with enabled: true, pipelineTemplate (an id or name) makes it run each task through that pipeline and null makes it work tasks itself. A template with a required parameter is refused

A parent or blocker the caller cannot see, a self-link, or a parent loop is a 400. Only an active (open or in-progress) blocker blocks; a done one is inert and stays in the array.

MethodEndpointDescription
GET/api/recurring-tasksList recurring tasks
POST/api/recurring-tasksCreate recurring task
PATCH/api/recurring-tasks/:idUpdate recurring task
DELETE/api/recurring-tasks/:idDelete recurring task
MethodEndpointDescription
POST/api/email/sendSend an email (gated)
POST/api/email/triageRun email triage (items, autoArchive, applyLabels → Gmail label / Outlook category Octipus/<category>)
GET/api/email/categoriesThe user’s triage categories and the presets
PUT/api/email/categoriesSave the user’s categories ({categories: {name: description}}, null resets)
POST/api/email/message/:id/save-draftSave a reply ({body}) as a draft in the mailbox, threaded under the message; nothing is sent
MethodEndpointDescription
GET/api/onboardingWhat is already set up, for the setup guide (models, CLI tools, areas, mailbox, connectors, chats, heartbeat, permissions, role agents, templates, saved progress). Install-wide details for admins only
PUT/api/onboarding/progressSave the guide’s progress (dismissed, useCases, answers without secrets, completed)
PUT/api/onboarding/standing-instructionsWrite (or append to) the pinned user-level heartbeat note ({body})
MethodEndpointDescription
GET/api/heartbeat/meThe caller’s heartbeat: enabled, intervalMinutes (null = the server’s), watchEmail, watchEmailSince, the server settings, the connected mailbox, whether standing instructions exist, and the last 10 outcomes of the inbox watch
PUT/api/heartbeat/meChange them: enabled, intervalMinutes (5–1440, null resets), watchEmail
MethodEndpointDescription
GET/api/capabilitiesList available capabilities and status
MethodEndpointDescription
GET/api/saml/:orgSlug/metadataRetrieve organization SAML metadata
POST/api/saml/:orgSlug/acsOrganization SAML assertion consumer service

SCIM (System for Cross-domain Identity Management)

Section titled “SCIM (System for Cross-domain Identity Management)”
MethodEndpointDescription
GET/api/scim/v2/UsersList users
POST/api/scim/v2/UsersCreate user in the token’s org (a member with that userName is returned as-is; one held outside the org is 409 uniqueness)
GET/api/scim/v2/Users/:idGet user
PATCH/api/scim/v2/Users/:idApply supported SCIM user updates; userName/emails only on an account the org alone holds (no other org, not an install admin, no own password), else 403
DELETE/api/scim/v2/Users/:idRemove the user from the token’s org; deactivate when no other org holds them
GET/api/scim/v2/GroupsList groups
MethodEndpointDescription
GET/api/admin/orgsList organizations (admin)
POST/api/admin/orgsCreate organization (admin)
GET/api/admin/usersList all users (admin)
POST/api/admin/usersCreate user (admin)
PATCH/api/admin/users/:idUpdate user (admin)
GET/api/admin/quotasList usage quotas (admin)
GET/api/admin/quotas/:userIdGet user quota details (admin)
PATCH/api/admin/quotas/:userIdUpdate user quota overrides (admin)
DELETE/api/admin/quotas/:userIdClear user quota overrides (admin)
POST/api/admin/impersonate/:userIdStart impersonation session (admin)
POST/api/admin/impersonate/stopStop impersonation session
GET/api/admin/impersonateList recent impersonation sessions (admin)
GET/api/admin/auditAudit log (admin)
GET/api/admin/permission-requestsPending tool permission requests of every user (admin); a space’s requests only for its members
POST/api/admin/permission-requests/:id/resolveAnswer someone else’s permission request: {approved, reason} (reason required, audited)
GET/api/admin/approvalsPending root-agent approvals of every user (admin); a space’s only for its members
POST/api/admin/approvals/:id/resolveAnswer someone else’s approval: {approved, reason, response?} (reason required, audited)
GET/api/admin/group-channelsEvery group-channel enrolment (admin)
PATCH/api/admin/group-channels/:idChange an enrolment’s mode, quiet hours or rate limit (admin, audited)
DELETE/api/admin/group-channels/:idRemove an enrolment (admin, audited)

Requests and approvals are answered by their requester on every other path (REST and the gateway); these two routes are the admin’s only way to answer for someone else.

MethodEndpointDescription
GET/api/me/workspacesMy personal workspaces (the default one is created on first access); spaces are listed under /api/spaces
POST/api/me/workspacesCreate one: {slug, name, isDefault?} (201)
PATCH/api/me/workspaces/:idRename: {name}
POST/api/me/workspaces/:id/defaultMake it my default workspace
DELETE/api/me/workspaces/:idDelete one of my workspaces
POST/api/me/workspaces/:id/transferGive it to another user: {recipientUserId} or {recipientUsername}; its content moves with it
GET/api/me/orgsMy organization memberships

Shared workspaces (SPACES.md). A caller who is not a member gets 404 for every space id; a member whose role lacks the action gets 403 (forbidden_role); last_owner, space_full, archived, not_purgeable and funding_off answer 409. Bodies reject unknown fields (422).

MethodEndpointWhoDescription
GET/api/spacesany userMy spaces with my role
POST/api/spacesper spaces.creationCreate a space {name}; the caller becomes its owner
GET/api/spaces/:idmemberName, my role, member count, archived, funding, sponsorUserId, sponsorModels
PUT/api/spaces/:id/fundingowner{mode?: "own" | "unattended" | "sponsored", sponsor?: "me" | null, sponsorModels?}; only the sponsor sets sponsorModels (their own model rows)
GET/api/spaces/:id/budgetmemberThe space’s budgets with this period’s spend; the member cap with my own share
PUT/api/spaces/:id/budgetowner{kind: "space" | "space_member", period: "day" | "month", limitUsd: number | null, warnRatio?}; null removes it
PATCH/api/spaces/:idownerRename {name}
POST/api/spaces/:id/archiveownerMake read-only and stop its agents
POST/api/spaces/:id/unarchiveownerUndo archive
DELETE/api/spaces/:idownerDelete for good; only archived for spaces.purgeAfterArchiveDays
GET/api/spaces/:id/membersmember{userId, username, role, joinedAt}
PATCH/api/spaces/:id/members/:userIdownerChange role {role, scope?}; scope = {rooms, folders} for guests (SPACES.md → Guests)
DELETE/api/spaces/:id/members/:userIdowner, or selfRemove a member, or leave
GET/api/spaces/:id/invitesownerInvites (never the token)
POST/api/spaces/:id/invitesowner{role, scope?, expiresInHours?, maxUses?} → {id, token, role, expiresAt, maxUses}; the token is shown once
DELETE/api/spaces/:id/invites/:inviteIdownerRevoke an invite of this space
GET/api/spaces/:id/activitymemberAudit rows of the space, newest first; ?limit=&before=
PUT/api/spaces/:id/agent-edit-modeowner{mode: "suggest" | "direct"}: whether the agent’s note writes become edit proposals
GET/api/spaces/:id/file-leasesmemberLive file leases (“Ben is editing”): {path, holderUserId, holderName, holderKind, expiresAt}
POST/api/spaces/:id/file-leaseseditor, owner{path, renew?} take (or renew) the lease on a space file; 409 lease_held with heldBy when someone else has it (or a directory above it, or a file under it)
DELETE/api/spaces/:id/file-leases?path=memberRelease your lease
GET/api/spaces/:id/connectorsmemberThe space’s connectors (GitHub, Atlassian, Linear): {id, name, kind, connected, connectedBy, connectedAt}; never a value
POST/api/spaces/:id/connectors/:connectorIdownerConnect: {token} for GitHub; an OAuth connector returns {url} for the popup, whose callback stores the tokens under the space
DELETE/api/spaces/:id/connectors/:connectorIdownerDisconnect (the space’s secrets of that connector are deactivated)
GET/api/spaces/:id/roomsmemberRooms I can enter, with unreadCount and muted
POST/api/spaces/:id/roomseditor, owner{title, visibility: "space" | "private", memberIds?}
GET/api/spaces/:id/rooms/:roomId/messagesroom access?before=&after=&limit= (message ids); posts and replies with authorUserId, authorName
POST/api/spaces/:id/rooms/:roomId/messagescommenter+{content, addressed?, clientId?}: REST fallback of room.post → {messageId, queuedPosition?, notQueued?}; /… → {commandResult}
PATCH/api/spaces/:id/rooms/:roomIdroom creator, owner{title?, visibility?}
GET/api/spaces/:id/rooms/:roomId/membersroom accessA private room’s members, or the space’s for an open one
GET/api/spaces/:id/rooms/:roomId/moderoom access{mode, quietHoursStart, quietHoursEnd, timezone, maxUnpromptedPerDay, minMinutesBetween, feedback: {up, down}}
PUT/api/spaces/:id/rooms/:roomId/moderoom creator, owner{mode?: "mention" | "listen" | "proactive", quietHoursStart?, quietHoursEnd?, timezone?, maxUnpromptedPerDay?, minMinutesBetween?}
PUT/api/spaces/:id/rooms/:roomId/messages/:messageId/feedbackroom access{value: 1 | -1 | null} on the agent’s unprompted post
POST/DELETE/api/spaces/:id/rooms/:roomId/members/:userIdroom creator, ownerPrivate rooms only; removal ends the member’s subscriptions and turns there
PATCH/api/spaces/:id/rooms/:roomId/meroom access{muted?, lastReadMessageId?}
GET/api/spaces/:id/memorymemberSpace memory entries, newest first
POST/api/spaces/:id/memoryeditor, owner{body} (≤ 500 characters)
DELETE/api/spaces/:id/memory/:entryIdeditor, ownerRetract an entry
GET/api/me/worksigned inMy open tasks assigned to me, across my spaces and personal workspaces: {groups: [{workspaceId, name, kind, tasks}]}
GET/api/invites/:tokenpublicPreview {spaceName, inviterName, role, expiresAt}; rate-limited per IP
POST/api/invites/:token/acceptsigned inJoin → {workspaceId, role, alreadyMember}; rate-limited per IP

In a space, POST /api/notes takes baseSha256 (the sha of the body the edit was made from — GET /api/notes/:id returns the live text and its sha as bodySha256 while the note is open in an editor). The edit is merged with what others wrote since, or refused with 409 {code: "stale", currentSha256}; a note over spaces.noteMaxBytes is a 413. A body write to an existing space note without baseSha256 is a 400 {code: "base_required"}: read the note first. Omitting body on an existing space note saves only its title, tags and kind. Line endings are stored as \n.

MethodEndpointWhoDescription
GET/api/notes/:id/revisionsmemberRevisions, newest first: {id, createdAt, origin, size, authors[], onBehalfOf}
GET/api/notes/:id/revisions/:revisionIdmemberOne revision with its body
POST/api/notes/:id/revisions/:revisionId/restoreeditor, ownerWrite that revision’s text as a new revision
POST/api/notes/:id/mergeeditor, owner{base, text}: merge a live editor’s text the server never got (typed offline, or unsent when the document was rebuilt) from base, the last server text it synced, as the member’s typing → {changed, merged, sha256, revisionId}; 409 stale on a clash (nothing applied)
GET/api/notes/proposals?noteId=&status=memberThe agent’s edit proposals
POST/api/notes/proposals/:proposalId/accepteditor, ownerApply it (merged through the live document); 409 {status: "stale", base, current, proposed} when it collides with a newer edit
POST/api/notes/proposals/:proposalId/rejecteditor, ownerClose it

In a personal workspace these routes answer 404.

MethodEndpointDescription
GET/api/swarm/nodes?rootSessionId=<id>List swarm nodes in a session (tree rehydration after WS replay ages out). Session owner or admin.
GET/api/swarm/nodes/:idFull node detail including result jsonb.
POST/api/swarm/nodes/:id/cancelCancel a node and all descendants. Runs AgentManager.stop on each plus a DB walk; returns { cancelled, nodeId, descendantIds, stoppedLive }.
MethodEndpointDescription
GET/api/trajectories?outcome=&from=&to=&limit=List trajectory runs (one per handleMessage). Filters by outcome (success / failure / partial / cancelled) and date range, capped at 1000.
GET/api/trajectories/:idSingle trajectory record.
MethodEndpointDescription
GET/api/skills/proposalsList pending skill proposals (detector-auto-generated).
POST/api/skills/proposals/:id/approvePromote a proposal to a skill. Body: { name?, role?, systemPrompt? }.
POST/api/skills/proposals/:id/rejectReject and suppress for 90 days.
MethodEndpointDescription
GET/api/knowledge/readinessRun the KB self-check (DB + embedding model + vector write probe). Returns 503 with { kb: { ready: false, reasons: [...] } } if the KB is not ready.
GET/api/knowledgeBrowse entries (lightweight, no vectors).
GET/api/knowledge/statsCounts per source type.
POST/api/knowledge/searchSearch (mode: hybrid / semantic / keyword). 503 if KB not ready.
GET/api/knowledge/:idFull entry.
DELETE/api/knowledge/:idDelete entry.
POST/api/knowledge/cleanupOrphan / stale / short / duplicate cleanup with optional dryRun.
GET/api/knowledge/cleanup-historyRecent cleanup runs.
POST/api/knowledge/indexIndex a file or directory. 503 if KB not ready.

Every knowledge route works on the caller’s own entries plus the product docs (another user’s entry is a 404). An admin adds ?scope=install to list, read, search, delete, clean up or count across the whole install, or to see every cleanup run; each such request writes a knowledge_install_access audit row. ?scope=install from a non-admin is a 403.

mode='graph' is available to agents through the knowledge tool, not on this route — see KNOWLEDGE-GRAPH.md.

Authored markdown notes — the knowledge graph’s Tier 2 surface. Full model in KNOWLEDGE-GRAPH.md. Every route works in the request’s workspace (X-Octipus-Workspace): it sees that workspace’s notes plus user-level ones, and creates new notes in it. No route takes a workspaceId in its body.

MethodEndpointDescription
GET/api/notesList notes. Query: kind, tag, includeArchived, limit (max 500).
POST/api/notesCreate or update a note (re-links [[wikilinks]]/#tags and re-indexes).
POST/api/notes/queryProperty query: { kind?, tag?, frontmatter?, sort?, order?, limit? }.
GET/api/notes/indexLightweight {id,title,slug,kind} list — the source for [[ autocomplete.
GET/api/notes/tagsTag → count across active notes.
POST/api/notes/captureAppend { text, date? } to the day’s daily note (the workspace’s, else an existing user-level one).
GET/api/notes/:idRead a note with its backlinks.
GET/api/notes/:id/suggestionsSemantically related, not-yet-linked entities (computed, not persisted).
PATCH/api/notes/:id/pin{ pinned: boolean }.
DELETE/api/notes/:idArchive (soft). ?hard=true also deletes the note’s chunks and edges.
MethodEndpointDescription
GET/api/graphGlobal mode: active notes + resolved edges (max 2000 nodes / 5000 edges). With entryType + entryId: local neighbourhood, hops clamped 1–5. Ghost (unresolved) edges are never returned.
GET/api/graph/canvasJSON Canvas projection of a neighbourhood. Query: entryType, entryId, hops (1–5).
MethodEndpointDescription
GET/api/mcp/serversList MCP servers with connection status (admin).
POST/api/mcp/serversAdd a server (admin).
POST/api/mcp/servers/:id/toggleEnable/disable.
PUT/api/mcp/servers/:id/exposureSet exposure and/or the whole toolExposure map (Tool exposure).
PUT/api/mcp/servers/:id/descriptionSet the one-line description agents see in the MCP SERVERS prompt section; "" clears it.
PUT/api/mcp/servers/:id/tools/:tool/exposureSet one tool’s exposure override; { "exposure": null } removes it.
POST/api/mcp/servers/:id/connectConnect manually.
POST/api/mcp/servers/:id/disconnectDisconnect.
DELETE/api/mcp/servers/:idRemove.
GET/api/mcp/toolsAll tools across connected servers.
GET/api/mcp/servers/:id/toolsTools for one server.
GET/api/mcp/circuitCircuit-breaker state for every server (admin).
POST/api/mcp/circuit/:serverId/resetForce-close a server’s breaker (admin).
MethodEndpointDescription
POST/api/voice/transcribeTranscribe audio (base64) to text
MethodEndpointDescription
GET/api/skills/topicsList skill-topic bindings
POST/api/skills/topicsCreate assignment
DELETE/api/skills/topics/:idDelete assignment
MethodEndpointDescription
GET/api/metricsSystem metrics (uptime, requests, etc.)
MethodEndpointDescription
GET/api/logs/recentGet recent logs
MethodEndpointDescription
GET/api/memoryGet memory state
DELETE/api/memory/:idDelete memory entry
MethodEndpointDescription
GET/api/searchGlobal search: the caller’s own sessions and hooks, models, skills, tools, and the caller’s knowledge
MethodEndpointDescription
GET/api/workspaceGet workspace info
PUT/api/workspaceUpdate workspace
MethodEndpointDescription
GET/api/settingsAll settings, grouped by category (secrets masked)
GET/api/settings/registryRegistry metadata for UI rendering
GET/api/settings/:keyOne setting by key (secrets masked)
PUT/api/settings/:keyUpdate one setting
PUT/api/settings/batchUpdate several settings
POST/api/settings/:key/resetReset one setting to its default

Group chats enrolled from inside the channel (CHANNELS.md). Owner-only, admins included (the admin routes above are the audited override); another user’s enrolment answers 404.

MethodEndpointDescription
GET/api/me/group-channelsMy enrolled group channels
PATCH/api/me/group-channels/:idSet mode (mention/listen/proactive), quiet hours, timezone and rate limits
DELETE/api/me/group-channels/:idRemove the enrolment
POST/api/me/group-channels/:id/bindBind the channel to a space I own: {workspaceId, acknowledged: true, roomId?}. roomId becomes the main thread; other threads get a room on first use. Audited (SPACES.md → Group channels bound to a space)
DELETE/api/me/group-channels/:id/bindUnbind it (the channel’s owner, or an owner of the space)
MethodEndpointDescription
GET/api/auth/channel-bindingsList channel bindings
POST/api/auth/channel-bindings/redeemRedeem a channel binding code
DELETE/api/auth/channel-bindings/:channelType/:externalIdRemove channel binding
MethodEndpointDescription
GET/api/devicesList devices
POST/api/devices/pair/generateGenerate device pairing code (valid 5 minutes; stored only as its SHA-256)
POST/api/devices/pair/redeemRedeem a pairing code for a mobile session (unauthenticated; each code redeems once)
DELETE/api/devices/:sessionIdRevoke device session

Tokens are shown once at creation; revoke and create a replacement to rotate. An empty scope list is full access. Restrict automation tokens to the scopes required by their routes; see src/security/scopes.ts for supported names.

MethodEndpointDescription
GET/api/auth/api-tokensList API tokens
POST/api/auth/api-tokensCreate token
DELETE/api/auth/api-tokens/:idRevoke token
MethodEndpointDescription
GET/api/auth/oauth/:provider/statusGet provider connection status
GET/api/auth/oauth/:provider/authorizeInitiate OAuth flow
GET/api/auth/oauth/:provider/callbackOAuth callback
POST/api/auth/oauth/:provider/disconnectDisconnect OAuth provider

A connector is a remote MCP server the user authorises once, per user, over OAuth 2.1 with PKCE. Octipus registers itself as a public client dynamically against the server’s own discovery document, so a connector is a definition file (id, name, MCP endpoint, discovery URL, scopes) and nothing else — there is no per-connector OAuth code. Built in today: Atlassian (Jira and Confluence) and Linear.

MethodEndpointDescription
GET/api/connectorsList available connectors with this user’s connection status
POST/api/connectors/:id/authorizeStart the OAuth flow; returns { url } to open in a popup
GET/api/connectors/:id/callbackOAuth redirect target; posts connector:connected to the opener
DELETE/api/connectors/:idDisconnect — deletes this user’s stored tokens

Tokens live in the vault under connector_<id>_access_token / _refresh_token / _token_expiry, per user; the dynamically registered client and its endpoints are system-scoped under connector_<id>_client_id / _auth_endpoint / _token_endpoint.

Every connected connector is reachable through the generic connector_list_tools / connector_call_tool pair. Atlassian additionally has named tools (atlassian_sites, jira_search, jira_get_issue, jira_create_issue, jira_update_issue, jira_comment, jira_transition_issue, jira_list_projects, confluence_search, confluence_get_page, confluence_create_page, confluence_update_page), held by the pm role. Those resolve the remote tool by capability rather than by a hard-coded name — an ordered list of candidate names and argument names, matched against the server’s own tools/list at call time — so a vendor rename does not silently kill the tool group. When nothing matches, the error names the remote tools that do exist.

MethodEndpointDescription
GET/api/pluginsList plugins
POST/api/plugins/:name/reloadReload installed plugin from disk
GET/api/plugins/:nameGet loaded plugin details

Outgoing webhook delivery is configured as a hook action; there is no separate outgoing-webhook CRUD API. See Hooks and Webhooks. POST /api/webhooks/:path receives configured webhook triggers.

MethodEndpointDescription
POST/api/hooks/incoming/:hookIdReceive inbound webhook (hook-configured authentication/signature checks)
MethodEndpointDescription
POST/api/channels/teams/webhookTeams webhook receiver
MethodEndpointDescription
POST/api/channels/whatsapp/webhookWhatsApp webhook receiver
MethodEndpointDescription
GET/api/evaluations/eval/runsList evaluator runs
POST/api/evaluations/eval/runStart evaluator run
GET/api/evaluations/eval/runs/:idGet evaluator run results
MethodEndpointDescription
POST/api/eval/runRun YAML eval scenarios
MethodEndpointDescription
GET/api/artifacts/_metaGet artifact host mode (subdomain or DNS-less)
GET/api/artifactsList artifacts in workspace
POST/api/artifactsCreate new artifact
GET/api/artifacts/spec/:slugOrIdGet full artifact spec for validation
GET/api/artifacts/:idGet artifact details
PUT/api/artifacts/:idUpdate artifact
DELETE/api/artifacts/:idDelete artifact
GET/api/artifacts/:id/versionsList artifact versions
POST/api/artifacts/:id/versions/:versionId/restoreRestore artifact to previous version
GET/api/artifacts/:id/data-sourcesList artifact data sources
POST/api/artifacts/:id/data-sourcesAdd data source
DELETE/api/artifacts/:id/data-sources/:sourceIdRemove data source
POST/api/artifacts/:id/refreshRefresh artifact data
GET/api/artifacts/:id/data/:sourceNameGet data from source
POST/api/artifacts/:id/share-linksCreate share link
GET/api/artifacts/:id/share-linksList share links
DELETE/api/artifacts/:id/share-links/:linkIdDelete share link
GET/api/artifacts/:id/feed.rssGet RSS feed
MethodEndpointDescription
GET/a/:slugServe hosted artifact page; DNS-less deployments also expose /artifacts/a/:slug

Connect to /gateway for real-time communication. All messages are JSON, Zod-validated.

First message must be auth within 5 seconds:

{
"type": "auth",
"method": "session_token",
"credentials": { "token": "<jwt>" },
"clientType": "webchat"
}

Auth methods:

  • session_token — credentials.token: a login session token (web, TUI after /login).
  • api_key — credentials.key: a personal API token (octi_…, Settings → API Tokens).
  • artifact_token — credentials.artifactId and credentials.token: the short-lived token an artifact embed page is served with. The connection is a viewer of that one artifact, not a user: it may only ping and (un)subscribe artifact:<id>. At most 50 viewer connections per artifact; each closes when its token expires (code 4001), and all close when the artifact is deleted or its visibility changes (code 4003; reload the page for a new token).

A user holds at most gateway.maxConnectionsPerUser (default 20) signed-in connections; the one over the cap gets auth_error Too many connections. A client frame larger than gateway.maxFrameBytes (default 262144 bytes, echoed in auth_ok.maxFrameBytes) closes the socket with code 1009.

Every authenticated connection has user trust; there is no local trust and no loopback exemption. Admin rights come from the user’s isAdmin flag in the database. A connection whose user is deactivated or whose admin flag changes is closed (codes 4001 and 4004) and must re-authenticate.

TypeDescription
authAuthentication handshake
chat.sendSend chat message (requires sessionId, content; optional workspaceId — one of yours — for a session the server has not seen yet, fileRefs, outputMode, attachments). A message sent while a turn of that session runs steers it (chat.message with injected: true)
chat.steerInject a message into the running turn of one of your sessions (a normal chat.send when none runs)
chat.interjectA side question answered alongside a running turn
commandExecute gateway command (name, optional args)
subscribepatterns: event-type patterns over the connection’s own events (e.g., ["agent.*"]); resources: resources to receive events of (e.g., ["artifact:<id>"]), each access-checked and answered with subscribed or a FORBIDDEN error
unsubscribeRemove event patterns and/or resources
permission.respondApprove/deny permission request
approval.respondApprove/deny pipeline approval
agent.stopStop one of the connection’s own user’s running agents (an admin included; another user’s agent is AGENT_NOT_FOUND)
voice.set{ sessionId, on } — voice mode for one of your sessions on this connection: lifecycle narration arrives as voice.speak events; cleared when the connection closes. Another user’s session is SESSION_NOT_FOUND
replay{ sessionId, afterEventId? } — the buffered events of one of your existing sessions after afterEventId. Another user’s or an unknown session is SESSION_NOT_FOUND
pingHeartbeat
TypeDescription
auth_okAuth success (includes connectionId, capabilities, serverTime, maxFrameBytes)
auth_errorAuth failure
eventGateway event (agent lifecycle, chat response, etc.); only the connection’s own user’s events, plus those of subscribed resources
subscribedResources of a subscribe that passed the access check
command.resultResult of a command
errorError with code and message
pongHeartbeat response with server time
permission.pending{ requests, approvals }: your open permission requests and root-agent approvals, after every subscribe with patterns (events raised while it is read follow it) and after a permission.respond that found the request already answered
replay{ sessionId, events, gap }: answer to replay; gap means the events cannot bridge what was missed — reload from REST

Events of note for clients (event.type): chat.response, chat.delta, chat.error (a failed turn, to every connection of the user), chat.message (injected steer, sideChannel answer, or proactive in-app delivery to webchat:<your id>), permission.request / permission.resolved, agent.approval_required / approval.resolved, document.*, model.install_progress, voice.speak, swarm.*, agent.*. A refused approval.respond answers error APPROVAL_NOT_FOUND (unknown, someone else’s, or already answered) or APPROVAL_EXPIRED.

Available via { "type": "command", "name": "<cmd>" }:

CommandDescription
/helpList available commands
/statusSession status and running agents
/abortCancel all running agents
/clearClear conversation
/compactCompact session context
/think [level]Set thinking depth (off/low/medium/high)
/verbose [on|off]Toggle verbose output
/usage [mode]Usage display (off/tokens/full)

The same protocol, framed as JSON lines over the process’s own stdin and stdout, for an IDE or a parent process that embeds Octipus as a subprocess:

octipus --stdio # or GATEWAY_STDIO=1

One line in is one client message (the auth line first, within 5 seconds), one line out is one gateway message; lines end in \n only. Logs go to stderr in this mode so the pipe carries nothing but protocol. Lines are handled strictly in order. The WebSocket endpoint stays up alongside; the stdio connection is one more client of the same hub, with the same rate limits, budgets and event-visibility rule. When stdin ends, the process shuts down.

MethodEndpointDescription
GET/api/gateway/statusHub status (connections, events)
GET/api/gateway/connectionsActive connections (admin)
GET/api/gateway/events/statsEvent bus metrics
GET/api/gateway/adaptersChannel adapter status
GET/api/health/timeServer time and timezone

/ws and /ws/permissions were removed (coworking S0d); the web app uses /gateway. The browser extension’s /ws/browser-bridge and the voice sockets (/voice, /voice/media/:provider) remain.

Octipus exposes an OpenAI-compatible surface at /v1 so off-the-shelf OpenAI SDKs can talk to it. Authenticate with a personal access token (Authorization: Bearer octi_…) exactly like /api. The api:chat scope is enforced on completions (unscoped tokens are full-access — see token scopes).

MethodEndpointDescription
GET/v1/modelsList available models: octipus/agent + every registry model.
POST/v1/chat/completionsChat completion (streaming and non-streaming).
  • octipus/agent (default when model is omitted) runs the latest user message through the full agent turn — the root agent with its tools, delegating to specialists when it needs one. The older octipus/orchestrator ID remains accepted as a compatibility alias. This mode is session-stateful: pass a stable user field (or an X-Octipus-Session header) to keep a conversation sticky; otherwise each call runs in a fresh ephemeral session.
  • A registry model id (e.g. gpt-4o, llama3.2, from GET /v1/models) is a single-turn passthrough to that provider — no tools, no session — and forwards the supplied text messages array. Provider-reported token usage is returned. This is a limited text chat-completions surface, not full OpenAI API compatibility (no multimodal content arrays or client tool schemas).
from openai import OpenAI
client = OpenAI(base_url="https://your-host/v1", api_key="octi_…")
# Root agent pipeline
resp = client.chat.completions.create(
model="octipus/agent",
messages=[{"role": "user", "content": "Summarize today's open PRs"}],
user="my-session-id", # optional: keep the conversation sticky
)
print(resp.choices[0].message.content)
# Passthrough to a specific model, streaming
for chunk in client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Write a haiku about octopuses"}],
stream=True,
):
print(chunk.choices[0].delta.content or "", end="")
Terminal window
curl https://your-host/v1/chat/completions \
-H "Authorization: Bearer octi_…" \
-H "Content-Type: application/json" \
-d '{"model":"octipus/agent","messages":[{"role":"user","content":"hello"}]}'
  • Agent-mode usage has no prompt/completion split: the observed total is reported in completion_tokens and total_tokens, with prompt_tokens: 0.
  • Streaming (stream: true) is protocol-correct SSE that chunks the final message text. Token-true streaming (per-delta) is a planned follow-up.
  • octipus/<role> model ids (forced single-role) are not yet wired and return 400 model_not_found; use octipus/agent or a registry model.
  • Errors use the OpenAI error envelope (invalid_request_error, authentication_error, server_error) so SDK error handling works.

Edit this page in the repository.