Skip to content

Trimus HTTP / JSON API Reference

Version: verified against v5.0.2

This document is the authoritative reference for Trimus's HTTP API. It is generated from — and verified against — the route table in cmd/server/routes.go and the handlers in internal/handler/. Every endpoint, method, path, auth requirement, and field below was checked against the code.

Trimus is a multi-tenant AI-agent control plane. The domain model is companies → agents → issues (a Kanban board), layered with approvals, skills, routines, notes, projects/goals, providers, secrets, plugins, A2A federation, MCP, budgets, and remote workers. The same server process renders a server-side HTMX UI and exposes this machine-callable API.

Scope. This reference focuses on the JSON API (the /api/... surface), the public endpoints (health, metrics, agent-card discovery, webhook ingress, worker bootstrap), the MCP and A2A JSON-RPC endpoints, and the SSE stream. The server also serves ~150 session-only HTMX/HTML routes (pages and …/ui/… form-post partials under /companies/..., /issues/..., /agents/..., /admin/...). Those are the browser UI and are intentionally not enumerated here — they are not part of the programmatic contract. Where a UI route is the only way to reach a capability, it is called out inline.


Table of contents


Conventions

Base URL

BASE=http://localhost:3100

PORT defaults to 3100. Bind host comes from TRIMUS_HOST (default: all interfaces). The URL agents use to reach the API is configured separately via TRIMUS_PUBLIC_URL. Replace $BASE with your own origin in every example.

Authentication

Trimus has three authentication schemes, applied per route group.

Scheme Header / mechanism Resolves to Used by
Bearer (API key) Authorization: Bearer sk_… an agent actor (agent_api_keys) or a board-user actor (board_user_api_keys) the JSON API (/api/...), MCP, A2A inbound
Session cookie staple_session cookie (set by POST /login) a user actor (with instance role) the HTML UI and the user-session JSON API
Worker key Authorization: Bearer sk_… (looked up in remote_workers) a remote worker the worker task-loop endpoints only

Implementation: internal/middleware/auth.go (BearerAuth, UIAuth) and internal/middleware/worker_auth.go (WorkerAuth).

How BearerAuth resolves a key. The token is SHA-256 hashed and looked up first in agent_api_keys (→ agent actor), then in board_user_api_keys (→ board_user actor). No match → 401 {"error":"invalid api key"}. A missing or malformed Authorization header → 401.

How UIAuth works. On the UI and user-session groups, UIAuth runs first: it validates a staple_session cookie into a user actor; if absent, it promotes a staple_key cookie into an Authorization: Bearer header and lets BearerAuth take over. A browser request to a non-/api/ path with no valid auth is redirected to /login.

Obtaining keys

Key type How to get it Format
Agent API key POST /api/companies/{companyId}/agents (when an agent creates an agent, a key is returned once in the response), or POST /api/companies/{companyId}/agent-api-keys for an additional key sk_ + 64 hex chars
Board-user API key minted by an operator/admin (board-user keys live in board_user_api_keys; created via the operator tooling) sk_ + 64 hex chars
Worker key POST /api/workers/register returns it once as api_key sk_ + 64 hex chars
User session POST /login (form-encoded email + password) sets the staple_session cookie (7-day lifetime) opaque cookie

API keys are generated by auth.GenerateAPIKey() (internal/auth/apikey.go): a sk_-prefixed, 64-hex-character string (32 random bytes). Only the SHA-256 hash is stored; the raw key is shown exactly once at creation. Lose it and you must mint a new one. (All three key classes — agent, board-user, worker — use this same generator, so every key starts with sk_.)

Errors

Errors are JSON with a single error string:

{ "error": "agent not found" }
Code Meaning
200 OK (read or update)
201 Created
204 OK, no body
303 See Other (form-driven UI redirects)
400 Malformed request / validation failure
401 Missing or invalid credentials
403 Authenticated but not authorized for this action
404 Not found — or present-but-not-yours (cross-company reads return 404, never 403, to avoid leaking existence)
409 Conflict (issue already checked out; webhook replay; non-terminal run)
413 Payload too large
429 Rate limited
5xx Server error
503 Dependency unavailable (/readyz not ready; encrypted write attempted with TRIMUS_ENCRYPTION_KEY unset)

IDs, timestamps, pagination

  • IDs are UUIDv4 unless noted. Issues also carry a human shortcode (e.g. ACME-42) in the UI; the JSON API addresses issues by UUID.
  • Timestamps are RFC 3339 / ISO-8601 UTC.
  • Lists return a JSON array directly (no envelope) for most resources; a handful wrap their payload in an object (e.g. GET /api/agents{"agents":[…]}). The shape is noted per endpoint where it matters.
  • Issue listing (GET /api/companies/{id}/issues) paginates with limit (default 50) and an opaque cursor query param — not offset.
  • JSONB fields (adapter_config, metadata, definition, payload, …) accept any JSON the column allows. Postgres may reorder JSONB keys; never byte-compare responses for equality.

Rate limits & body size

  • Rate limit: RATE_LIMIT_RPM requests/minute per limiter (default 300), applied to the public, SSE, user-session, and bearer API groups. Over-limit → 429. Client IP is taken from X-Forwarded-For only when the peer is in TRIMUS_TRUSTED_PROXIES (CIDR allowlist); otherwise the socket address is used.
  • Body cap: 1 MiB for JSON endpoints (and for POST /api/workers/register). Multipart uploads (attachments, files, assets, logos) have their own caps — see TRIMUS_ATTACHMENT_* and TRIMUS_MAX_UPLOAD_SIZE (default 25 MB). Over-cap → 413.

Cross-company isolation

Company-scoped resources resolve the caller's company from the auth context. Any {companyId} in the path is checked against it (middleware.RequireCompanyAccess); instance admins may access any company. Mismatches return 404 (not 403) so the API never confirms the existence of another tenant's data. Tenant isolation is also enforced at the database layer via Postgres Row-Level Security (the staple_app role + WithTenantScope).


Route groups & middleware

Global middleware on every request (cmd/server/main.go): RequestID → OpenTelemetry tracing → structured logging.

Each group then layers additional middleware:

Group Middleware stack Auth
Static / root none (/static/*, / → 301 /companies)
Public rate-limit none
Auth (login) CSRF none (these are the auth entry points)
OAuth none
UI pages + …/ui/… forms CSRF, 1 MiB body, UIAuth, BearerAuth, then per-route RequireRole / RequireInstanceAdmin session cookie (or bearer)
SSE rate-limit, UIAuth, BearerAuth session cookie or bearer
User-session JSON API CSRF, 1 MiB body, rate-limit, UIAuth, BearerAuth session cookie or bearer
Bearer JSON API 1 MiB body, rate-limit, BearerAuth bearer API key
Remote-worker task loop (within bearer group) WorkerAuth worker key

Company-scoped routes under /api/companies/{companyId}/… additionally apply RequireCompanyAccess and a role gate:

  • Viewer+ (admin, member, viewer) — all reads/lists.
  • Member+ (admin, member) — create/update for issues, agents, routines, approvals, projects, goals, skills, notes, chats, files, cost events, etc.
  • Admin — secrets, providers, budget policies, portability, members, invites, plugin creation, logo, feedback export, agent defaults.

A few mutation endpoints deliberately self-gate inside the handler instead of via RequireRole — plugin approval transitions (allow/ban/rescind-ban) and secret reveal — so a denied attempt still writes an audit row before responding 403.


Public endpoints (no auth)

All public endpoints are rate-limited by the shared public limiter.

Health & metrics

Method Path Purpose
GET /api/health Legacy DB-ping health. 200 {"status":"ok","db":"ok"}; 503 when the DB is unreachable.
GET /healthz Liveness. Always 200 {"status":"alive","version":"…","commit":"…"} while the HTTP server responds.
GET /readyz Readiness. 200 when ready; 503 when any blocking check fails. JSON body with a per-check breakdown (see below).
GET /healthz/live Plain-text liveness alias. 200 + body OK\n.
GET /healthz/ready Plain-text readiness alias. 200 + ready\n, or 503 + not ready: <reasons>\n.
GET /metrics Prometheus exposition. Unauthenticated by default (Prometheus convention) — gate via network policy / reverse proxy, or set TRIMUS_METRICS_TOKEN to require Authorization: Bearer <token> (a missing or non-matching token → 401).

/readyz response shape (internal/handler/health.go):

{
  "status": "ready",
  "version": "v5.0.2",
  "commit": "abc1234",
  "checks": [
    { "name": "db_ping",        "ok": true, "duration_ms": 3,
      "pool_acquired": 1, "pool_total": 4, "pool_max_open": 10,
      "pool_idle": 3, "pool_utilization_pct": 10.0 },
    { "name": "workers",        "ok": true, "duration_ms": 0,
      "detail": "12 worker(s) registered" },
    { "name": "worker:heartbeat","ok": true, "ticks": 42, "errors": 0,
      "error_rate_pct": 0.0, "detail": "healthy (interval=30s)" },
    { "name": "channel:slack",  "ok": true, "detail": "healthy (last_poll=4s ago)" },
    { "name": "disk",           "ok": true, "pct_used": 41.0,
      "total_bytes": 0, "free_bytes": 0 },
    { "name": "backup",         "ok": true, "since_hours": 6.2, "threshold_hours": 36 }
  ],
  "recent_check_timings": { "db_ping": { "p50_ms": 2, "p95_ms": 5, "p99_ms": 9, "count": 100 } }
}

Readiness checks: db_ping (+ pool-utilization escalation), workers (aggregate + per-worker staleness and error-rate), channel:<name> (Slack / Telegram adapter freshness), disk (root-FS pressure), and backup (freshness of the last successful backup). Each check can be tuned or disabled via TRIMUS_READYZ_* env vars; the backup check is opt-out via TRIMUS_READYZ_BACKUP_CHECK=disable.

Authentication & OAuth

Method Path Purpose
GET /login Login page (HTML).
POST /login Form login (email, password). On success sets the staple_session cookie (7 days) and redirects 303 → /companies. Subject to login lockout (429-style throttling with Retry-After). CSRF-protected.
POST /logout Clears the session cookie; redirects to /login. CSRF-protected.
GET /auth/google · /auth/google/callback Google OAuth start / callback. Active only when TRIMUS_OAUTH_GOOGLE_* is configured.
GET /auth/github · /auth/github/callback GitHub OAuth start / callback. Active only when TRIMUS_OAUTH_GITHUB_* is configured.
GET /auth/oidc · /auth/oidc/callback Generic OIDC SSO start / callback. Active only when TRIMUS_OIDC_* is configured (404 otherwise).
curl -s -i -X POST "$BASE/login" \
  --data-urlencode 'email=admin@example.com' \
  --data-urlencode 'password=...'
# 303 See Other, Set-Cookie: staple_session=...

Agent-card discovery (A2A)

Public Agent Card metadata for Agent-to-Agent federation. The cards declare the apiKey security scheme a client must satisfy to send a task to the per-agent /a2a endpoint.

Method Path Purpose
GET /companies/{companyId}/.well-known/agent-card.json Directory of the company's externally-available agents.
GET /companies/{companyId}/agents/{agentId}/agent-card.json A single agent's card.

Remote-worker bootstrap

Intentionally unauthenticated — a freshly-booting worker has no key yet (registration is what issues it). Only register is public; every other worker endpoint — including GET /api/workers/operating-prompt — requires the worker key (see Remote workers).

Method Path Purpose
POST /api/workers/register Register a worker; returns its key + operating prompt once. 1 MiB body cap.
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"name":"gpu-worker-1","tags":["gpu","heavy"]}' \
  "$BASE/api/workers/register"

Response (201):

{
  "worker_id": "550e8400-e29b-41d4-a716-446655440000",
  "api_key": "sk_0a1b2c…",
  "operating_prompt": "# Trimus Remote Worker Operating Contract\n…",
  "prompt_version": "1.4.0"
}

api_key is shown once. Registration is idempotent: send an existing worker key as a Bearer token and the call resumes that worker (HTTP 200, same worker_id, no new api_key) instead of creating a duplicate row; first-time callers get 201 with a fresh key as shown above.

When TRIMUS_REQUIRE_WORKER_APPROVAL=true the response instead carries "status":"pending_approval" and no operating_prompt; the returned api_key stays inert (WorkerAuth rejects pending workers) until an instance admin approves the worker on /admin/workers, after which the next register call resumes as approved and delivers the prompt. A rejected worker's re-register returns 403. Registration is capped by TRIMUS_MAX_REMOTE_WORKERS (default 10000; at capacity → 503).

Instance admins can also pre-provision a worker from the browser (#335): /admin/workers → Register worker mints the key server-side, shows it once, and generates a ready-to-paste env file. Workers minted there are born approved (the admin's action is the admission decision) and count against the same cap; the shipped binary's boot-time register call then resumes them via the idempotency described above. Session-authed UI route — not part of the JSON API tables.

See Remote workers for the task-claim loop and REMOTE_WORKERS.md for the full protocol.

Webhook ingress

Method Path Purpose
POST /api/routine-triggers/public/{publicId}/fire Fire a routine via webhook.

Auth is per-trigger, not bearer. When the trigger's signing mode is hmac, the caller must send header X-Staple-Signature = hex HMAC-SHA256 of the raw request body keyed by the trigger secret. When the mode is hmac_ts, the caller must ALSO send X-Staple-Timestamp (Unix seconds); the signature is over "<timestamp>.<body>" and a timestamp more than 300 s from the server clock is rejected (401) — this makes a captured request un-replayable once it ages out (the mode is read server-side, so the timestamp can't be stripped to downgrade). Replay protection also rejects a body whose SHA-256 was seen within the replay window (default 300 s, configurable per-trigger).

Outcome Status
Fired 200 {"status":"<enqueued|coalesced|skipped>"}
Replay detected 409 {"error":"replay_rejected"}
Missing / invalid signature 401
Trigger not found or disabled 404
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -s -X POST -H "X-Staple-Signature: $SIG" -H 'Content-Type: application/json' \
  -d "$PAYLOAD" \
  "$BASE/api/routine-triggers/public/$PUBLIC_ID/fire"

Plugin webhook delivery (POST /api/companies/{companyId}/plugins/webhooks/{path}) is not public — it sits under the bearer API group. See Plugins.

Role default prompts

Method Path Purpose
GET /api/roles/{role}/default-prompt The built-in system prompt for a role (e.g. cto, engineer, pm). Returns JSON {"prompt": "..."}.

Roles with shipped prompts: ceo, cfo, cio, clo, cmo, cro, cto, designer, devops, engineer, general, pm, qa, researcher, scout, triage.


Authenticated resources

Everything below requires authentication. Unless a row says otherwise:

  • Auth is a bearer API key (agent or board-user). Endpoints that the browser also calls (feedback, /api/me, /api/users, company-user management) additionally accept a session cookie and are CSRF-protected — these are noted as session/bearer.
  • Company-scoped routes (/api/companies/{companyId}/…) resolve the caller's company from the auth context, apply RequireCompanyAccess, and a role gate (Viewer+/Member+/Admin). A {companyId} that isn't yours → 404.
  • Resource-scoped routes (/api/issues/{id}, /api/chats/{id}, …) carry no companyId, so the handler loads the row and checks the actor's company against it; cross-tenant → 404 (or 403 for a few write paths).
  • Request bodies are JSON (1 MiB cap) unless marked multipart. Field names below are the exact JSON keys.

Identity & profile

Self-service profile for the user behind a session (these reject agent / board-user keys with 401 "not authenticated as user").

Method Path Auth Purpose
GET /api/me session/bearer (user) Current user. Returns {id, email, display_name, instance_role, is_active, last_login_at, created_at}.
PATCH /api/me session/bearer (user) Update own profile. Body: email, display_name. Changing email invalidates all other sessions (SEC-018). Returns {id, email, display_name, instance_role}.
POST /api/me/change-password session/bearer (user) Body: current_password, new_password (≥ 8 chars). 403 if current password wrong; on success kills all other sessions. Returns {"message":"password changed"}.

Users (instance-admin)

/api/users/* is gated by RequireInstanceAdmin. Session or bearer.

Method Path Purpose · key fields
GET /api/users List all users.
POST /api/users Create user. Body: email, password (≥ 8), display_name (all required), instance_role. 201 → user.
GET /api/users/{userId} One user.
PATCH /api/users/{userId} Body: email, display_name, is_active, instance_role (all optional). Identity-relevant changes kill the target's sessions; a true→false is_active also auto-revokes their channel pairings.
DELETE /api/users/{userId} Remove a user.
POST /api/users/{userId}/reset-password Body: new_password (≥ 8). Kills all the target's sessions.

User object fields: id, email, display_name, instance_role, is_active, last_login_at, created_at, updated_at, slack_user_id?, telegram_user_id? (password_hash is never serialized).

Companies

Method Path Auth Purpose · key fields
POST /api/companies bearer, instance-admin Create. Body: name, slug (required; slug = ^[a-z0-9]+(-[a-z0-9]+)*$). 201 → company.
PATCH /api/companies/{companyId} bearer, admin Body: name, slug, home_dir (all optional).
DELETE /api/companies/{companyId} bearer, admin 200 {"deleted":"<id>"}.

Company object: id, name, slug, created_at, updated_at, home_dir?, agent_default_adapter_type, agent_default_adapter_config, agent_default_heartbeat_enabled, agent_default_heartbeat_interval_sec.

Company reads (get/list) are served by the UI/dashboard routes; the JSON API exposes companies for write only plus the company-scoped sub-resources below.

Three company-scoped, instance-admin capabilities are reachable only via session UI routes, with no JSON twin: the egress DNS allowlist (/companies/{companyId}/egress, …/egress/create, …/egress/{id}/delete), the Slack knowledge-ingestion channel allowlist (/companies/{companyId}/slack-channels, …/slack-channels/available, …/slack-channels/create, …/slack-channels/delete), and Compiled Knowledge review/promote (/companies/{companyId}/compiled-knowledge, …/compiled-knowledge/{articleId}/promote).

Company members, invites & join requests

Company-scoped. Reads are Viewer+; member/invite writes are admin.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/members viewer+ List memberships (id, company_id, user_id, role, joined_at).
POST /api/companies/{companyId}/members admin Add member. Body: user_id (required), role (admin/member/viewer).
PATCH /api/companies/{companyId}/members/{userId} admin Change role.
DELETE /api/companies/{companyId}/members/{userId} admin Remove member.
GET /api/companies/{companyId}/invites viewer+ List invites.
POST /api/companies/{companyId}/invites admin Create invite. Body: role, expires_in_hours (default 72). Returns invite incl. token.
POST /api/invites/{token}/accept any auth user Accept an invite token → creates a membership.
GET /api/companies/{companyId}/join-requests viewer+ List a company's join requests (members/admins).
POST /api/join-requests any auth user Self-service request to join a company you are not a member of. Body: company_id (required), message?. Returns 404 (unknown company), 409 (already a member), or the existing pending request (200, idempotent).
PATCH /api/join-requests/{id} bearer, admin Resolve. Body: status = approved|rejected. Approval creates a member membership.

The company-user management endpoints below live in the session JSON-API group (cookie or bearer) and are admin-gated; they overlap functionally with /members but key the role off ValidMembershipRoles and default to member:

Method Path Purpose
GET /api/companies/{companyId}/users List company users.
POST /api/companies/{companyId}/users Body: user_id, role.
PATCH /api/companies/{companyId}/users/{userId} Body: role (required, validated).
DELETE /api/companies/{companyId}/users/{userId} Remove.

Invite object: id, company_id, token, role, created_by?, expires_at, accepted_by?, accepted_at?, created_at. Join-request object: id, company_id, user_id, status, message?, resolved_by?, resolved_at?, created_at.

Agents

Create/test are company-scoped Member+. Mutations on /api/agents/{agentId} carry no companyId and self-check the agent's company.

Method Path Role Purpose · key fields
GET /api/agents bearer List agents in the caller's company; ?name=<exact> filters (case-insensitive). Returns {"agents":[…]} (empty list, not 404).
POST /api/companies/{companyId}/agents member+ Create/hire an agent. See body below. When the caller is an agent, response is {agent, api_key} (key shown once); otherwise the bare agent. 201.
PATCH /api/agents/{agentId} bearer Update. All fields optional & pointer-typed: name, role, status, title, description, reports_to, home_dir, adapter_type, adapter_config, provider_id, model, system_prompt, heartbeat_enabled, heartbeat_interval_sec.
DELETE /api/agents/{agentId} bearer Delete; clears reports_to pointers at it first. 204.
POST /api/agents/{agentId}/pause bearer Body: reason?.
POST /api/agents/{agentId}/resume bearer
POST /api/agents/{agentId}/heartbeat/invoke bearer Trigger one heartbeat run for any agent in the company. 201 → run.
GET /api/agents/{agentId}/runtime-state bearer {agent_id, status, current_run_id?, last_heartbeat_at?, last_error?, session_id?, session_cwd?, session_updated_at?, updated_at}.
GET / POST /api/agents/{agentId}/task-sessions bearer List / create. Create body: issue_id (required), run_id?.
PATCH /api/task-sessions/{id} bearer Body: status (active/completed/abandoned), summary?, run_id?.
GET / PATCH /api/agents/{agentId}/permissions bearer Get grants / set them. PATCH body is a flat { "<permission_key>": bool, … } map; unknown keys → 400.
GET /api/agents/{agentId}/configuration bearer The agent's effective configuration as a raw JSON object.
GET /api/agents/{agentId}/configuration/revisions bearer Audit trail: [{id, agent_id, config_json, reason?, created_by?, created_at}].
POST /api/companies/{companyId}/agent-api-keys member+ Mint an extra key for an existing agent. Body: agent_id (required), label?. Returns {id, agent_id, company_id, key_prefix, label, raw_key, created_at}raw_key once.
GET /api/agents/{agentId}/api-keys bearer List the agent's key metadata (never key material). Re-resolves the owning company in-handler; cross-company → 404.
DELETE /api/agent-api-keys/{keyId} bearer Revoke a key (JSON twin of the per-row UI revoke). Re-resolves the owning company in-handler; cross-company → 404.
POST /api/companies/{companyId}/agents/{agentId}/test-adapter member+ Live adapter smoke-test (LLM/Claude).
POST /api/companies/{companyId}/agents/{agentId}/test-codex-adapter member+ Codex-CLI adapter smoke-test.
POST /api/companies/{companyId}/agents/{agentId}/clear-session member+ Drop the persisted CLI session id/CWD.
GET /api/companies/{companyId}/agents/{agentId}/direct-reports viewer+ Agents reporting to this one.
GET /api/companies/{companyId}/org-tree viewer+ Full reporting tree.

Create-agent body (POST …/agents): name (required), role (must be a valid role — see Role default prompts), adapter_type, adapter_config (JSON), title, description, reports_to, system_prompt, provider_id, model, heartbeat_enabled, heartbeat_interval_sec. Omitted adapter/heartbeat fields fall back to the company's agent defaults; omitting system_prompt applies the role's default prompt.

Agent object (selected fields): id, company_id, name, role, status (active/paused/idle/running/error/pending_approval/terminated), adapter_type, adapter_config, title?, description?, system_prompt?, heartbeat_enabled, heartbeat_interval_sec, max_concurrent_runs, max_issues_per_run, reports_to?, home_dir?, provider_id?, model?, externally_available, blocked_notify_enabled, auto_extract_findings, parent_agent_id?, subagent_depth, spawn_reason?, current_prompt_version_number, gepa_auto_promote, gepa_provider_override?, created_at, updated_at.

Agent role-specific setters (externally-available, auto-extract-findings, GEPA auto-promote/provider-override, prompt-version promote/restore, accept/reject GEPA proposal, per-adapter heartbeat config) are UI-only form-posts under /agents/{agentId}/ui/… (session, member+). They are not in the JSON API; the equivalent state is reachable via PATCH /api/agents/{id} where a column exists.

Agent self-service (/api/agents/me)

For the agent behind the bearer key (reject non-agents with 401).

Method Path Purpose
GET /api/agents/me The calling agent's own record.
GET /api/agents/me/inbox-lite Actionable assigned issues (wakeup-triggered first), consuming pending wakeups. Returns {issues:[…], wakeup_requests:[…]}.
POST /api/agents/me/heartbeat/invoke Self-trigger a heartbeat run. 201 → run ({id, company_id, agent_id, issue_id?, status, started_at?, finished_at?, error?, …}).

Agent-scoped filesystem skills and files (same shapes as the company versions in Filesystem skills / Files):

Method Path
GET / POST /api/agents/{agentId}/fs-skills
GET / PUT / DELETE /api/agents/{agentId}/fs-skills/{slug}
GET / POST /api/agents/{agentId}/files
GET /api/agents/{agentId}/files/* (download; nested paths supported, #365)
DELETE /api/agents/{agentId}/files/{filename}
GET /api/companies/{companyId}/agents/{agentId}/home{home_dir, is_override}

Per-company agent defaults

Applied by CreateAgent when adapter/heartbeat fields are omitted.

Method Path Role Purpose
GET /api/companies/{companyId}/agent-defaults viewer+ Current defaults.
PATCH /api/companies/{companyId}/agent-defaults admin Body: adapter_type, adapter_config (JSON), heartbeat_enabled, heartbeat_interval_sec (all optional). Returns {adapter_type, adapter_config, heartbeat_enabled, heartbeat_interval_sec}.

Issues

Listing/creating is company-scoped (Viewer+/Member+). The per-issue routes under /api/issues/{issueId} carry no companyId; each handler loads the issue and checks the caller's company, returning 404 on a cross-tenant miss.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/issues viewer+ List. Query: status, assigneeAgentId, projectId, q (fuzzy pg_trgm), limit (default 50), opaque cursor. Returns a bare array.
POST /api/companies/{companyId}/issues member+ Create. JSON body → bare issue. multipart body → {issue, attachments[], attachment_errors[]?} (see below). 201. Auto-triggers the assignee.
GET /api/issues/{issueId} bearer Issue detail: the issue plus ancestors[], labels[], comment_count.
PATCH /api/issues/{issueId} bearer Update. Optional: title, body_markdown, status, priority, assignee_agent_id, project_id, goal_id. Logs activity; auto-unblocks parent on terminal status.
DELETE /api/issues/{issueId} bearer 204.
POST /api/issues/{issueId}/checkout bearer Atomic claim by the calling agent. 409 {"error":"conflict","message":"already checked out"} if held.
POST /api/issues/{issueId}/release bearer Release the lock. 403 if you don't hold it (or can't see the issue).
POST /api/issues/{issueId}/trigger-agent bearer Run the issue's assigned agent now. 400 if no assignee. 201 → run.
POST /api/issues/{issueId}/promote-routine bearer Promote an issue into a recurring routine. See Routines.
POST /api/issues/{issueId}/convene-council bearer Explicitly convene the multi-lens Council orchestrator on this issue (JSON twin of the UI "Convene Council" button). 201 → council session; 409 (+ session_id) if one is already running.
POST / DELETE /api/issues/{issueId}/labels/{labelId} bearer Add (POST) / remove (DELETE) a label on the issue.

Create-issue JSON body: title (required), body_markdown?, status, priority, assignee_agent_id?, project_id?, goal_id?, parent_issue_id?. Multipart: form fields title (required), priority, status, body_markdown, project_id, plus one or more file parts (per-file cap TRIMUS_ATTACHMENT_MAX_UPLOAD_BYTES, count cap …_MAX_FILES_PER_REQUEST; oversized/over-count → 413).

Valid enums. Status: backlog, todo, in_progress, in_review, done, cancelled, blocked. Priority: critical, high, medium, low.

Issue object: id, company_id, project_id?, goal_id?, parent_issue_id?, title, body_markdown?, status, priority, assignee_agent_id?, checked_out_by_agent_id?, checked_out_at?, origin_kind, origin_id?, identifier?, issue_number?, created_at, updated_at, awaiting_input, needs_routing, needs_human_attention, blocked_at?, blocked_reason?, blocked_by_agent_id?.

Labels are company-scoped:

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/labels viewer+ List labels (id, company_id, name, color?, created_at).
POST /api/companies/{companyId}/labels member+ Create. Body: name (required), color?.
DELETE /api/companies/{companyId}/labels/{labelId} member+ Delete a label.

Comments

All under /api/issues/{issueId} (bearer; company checked on the issue).

Method Path Purpose · key fields
GET /api/issues/{issueId}/comments List comments.
POST /api/issues/{issueId}/comments Add. Body: body_markdown (required), resume_after? (when true + issue blocked + human author → flips to in_progress). @agent mentions create wakeup requests; posting auto-triggers the assignee (unless author == assignee). 201.
POST /api/issues/{issueId}/comments/{commentId}/pin Pin/unpin. Optional body {"pinned":bool}; no body toggles. Returns {id, is_pinned}.

Comment object: id, issue_id, author_agent_id?, author_user_id?, body_markdown, created_at, is_pinned, author_user_name?.

Documents, attachments, work products, relations

All anchored on /api/issues/{issueId} (bearer). Documents are keyed, revision-tracked rich text; work products track external artifacts (PRs, deploys, …); relations link issues.

Documents

Method Path Purpose · key fields
GET /api/issues/{issueId}/documents List documents.
PUT /api/issues/{issueId}/documents/{key} Upsert. Body: title, body. Creates a new revision.
GET /api/issues/{issueId}/documents/{key} One document.
GET /api/issues/{issueId}/documents/{key}/revisions Revision history.
POST /api/issues/{issueId}/documents/{key}/revisions/{revisionId}/restore Restore a prior revision (append-only).

Document object: id, company_id, issue_id, document_id, key, title, latest_body, latest_revision_id?, latest_revision_number.

Attachments

Method Path Purpose · key fields
GET /api/issues/{issueId}/attachments List.
POST /api/issues/{issueId}/attachments Multipart upload (file). Text is extracted into extracted_text for agent context. Caps via TRIMUS_ATTACHMENT_*; over-cap → 413.
GET /api/attachments/{id}/download Stream the bytes.
DELETE /api/attachments/{id} Delete (file + row).

Attachment object: id, company_id, issue_id, filename, content_type, size_bytes, storage_key, created_by_agent_id?, created_by_user_id?, created_at.

Work products

Method Path Purpose · key fields
GET /api/issues/{issueId}/work-products List.
POST /api/issues/{issueId}/work-products Create. Body: title (required), type, provider?, external_id?, url?, status, review_state, is_primary, health_status?, summary?, metadata (JSON), project_id?, created_by_run_id?. Invalid type/review_state400.
PATCH /api/work-products/{id} Body: title, url, status, review_state, is_primary, health_status, summary (optional).
DELETE /api/work-products/{id} Delete.

Work-product object: id, company_id, project_id?, issue_id, type, provider?, external_id?, title, url?, status, review_state, is_primary, health_status?, summary?, metadata, created_by_run_id?, created_at, updated_at.

Relations & execution decisions

Method Path Purpose · key fields
GET /api/issues/{issueId}/relations List relations.
POST /api/issues/{issueId}/relations Create. Body: target_issue_id (required), relation_typeblocks/depends_on/relates_to. Self-relation → 400.
DELETE /api/issue-relations/{id} Delete a relation.
GET /api/issues/{issueId}/execution-decisions List decisions.
POST /api/issues/{issueId}/execution-decisions Create. Body: decision_typeauto_assign/auto_approve/escalate/skip (required), reason?, metadata (JSON).

Relation object: id, source_issue_id, target_issue_id, relation_type, created_by_agent_id?, created_by_user_id?, created_at.

Issue inbox & read state

Per-issue read tracking and inbox archive, plus the company inbox feed. All bearer; company resolved on the issue/row.

Method Path Purpose
POST / DELETE /api/issues/{issueId}/read Mark read (POST) / unread (DELETE) for the caller.
POST / DELETE /api/issues/{issueId}/archive-inbox Archive (POST) / unarchive (DELETE) the issue from the inbox.
GET /api/companies/{companyId}/unread-issues Viewer+. Issues unread by the caller.
GET /api/companies/{companyId}/inbox-dismissals Viewer+. List dismissed inbox item keys.
POST /api/companies/{companyId}/inbox-dismissals Member+. Dismiss an inbox item.
DELETE /api/companies/{companyId}/inbox-dismissals/{itemKey} Member+. Restore a dismissed item.

The rendered inbox itself (/companies/{id}/inbox) is a UI page.

Approvals

Agents create approvals; board users / human members resolve them (agents are blocked from resolving inside ResolveApproval). The /api/approvals/{id} routes self-check company on the loaded row, so no extra role middleware is applied.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/approvals viewer+ List.
POST /api/companies/{companyId}/approvals member+ Create. Body: type, title (required), body_markdown?. 201.
DELETE /api/companies/{companyId}/approvals/resolved member+ Bulk-delete every approved+rejected approval (pending untouched).
GET /api/approvals/{approvalId} bearer One approval.
PATCH /api/approvals/{approvalId} bearer (not agent) Resolve. Body: statusapproved/rejected (required), resolution?.
DELETE /api/approvals/{approvalId} bearer Delete one approval.
GET / POST /api/approvals/{approvalId}/comments bearer List / add threaded comment (body_markdown required).
GET / POST /api/approvals/{approvalId}/issues bearer List / link (issue_id) related issues.

Approval object: id, company_id, type, status, title, body_markdown?, requested_by_user_id?, requested_by_agent_id?, resolved_at?, resolution?, metadata, created_at, updated_at.


Projects & project workspace files

List/create are company-scoped Member+; per-project routes self-check.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/projects viewer+ List projects.
POST /api/companies/{companyId}/projects member+ Create. Body: name (required), description, status, color, shortname, goal_id.
GET /api/projects/{projectId} bearer One project.
PATCH /api/projects/{projectId} bearer Body: name, description, status, color, shortname, goal_id (optional).
DELETE /api/projects/{projectId} bearer Archive (soft-delete; sets archived_at).
GET /api/projects/{projectId}/goals bearer Goals linked to the project.
POST / DELETE /api/projects/{projectId}/goals/{goalId} bearer Link / unlink a goal.

Valid project status: backlog, planned, in_progress, completed, cancelled. Project object: id, company_id, name, description?, status, color?, shortname?, goal_id?, working_dir?, archived_at?, created_at, updated_at.

Project workspace files

A per-project file tree the claude_local adapter uses as a working dir. The {path} wildcard accepts subdirectories. Resolves project.working_dir or $TRIMUS_HOME/companies/{cid}/projects/{pid}/files. PUT body is the raw file bytes (cap home.MaxProjectFileBytes; over-cap → 413).

Method Path Purpose
GET /api/projects/{projectId}/files List files (recursive); [{name, size, modified_at}].
POST /api/projects/{projectId}/files Multipart upload.
GET /api/projects/{projectId}/files/* Download one file.
PUT /api/projects/{projectId}/files/* Write raw bytes to a path.
DELETE /api/projects/{projectId}/files/* Delete a path.

Goals

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/goals viewer+ List goals.
POST /api/companies/{companyId}/goals member+ Create. Body: title (required), description, level, status, project_id, parent_goal_id.
GET /api/goals/{goalId} bearer One goal.
PATCH /api/goals/{goalId} bearer Body: title, description, level, status, project_id, parent_goal_id (optional).
DELETE /api/goals/{goalId} bearer Delete. 204 on success, 404 when missing/out of scope. Linked issues, projects, routines, and child goals survive with their goal reference cleared; project-goal links cascade.
GET /api/goals/{goalId}/projects bearer Projects linked to the goal.

Valid goal level: company, team, agent, task. Valid status: planned, active, achieved, cancelled. Goal object: id, company_id, project_id?, parent_goal_id?, title, description?, level, status, created_at, updated_at.

Routines & triggers

Routines are recurring issue templates fired by cron or webhook triggers. List/create are company-scoped Member+; per-routine routes self-check.

Method Path Purpose · key fields
GET /api/companies/{companyId}/routines viewer+ — list.
POST /api/companies/{companyId}/routines member+ — create. Body: title (required), description, assignee_agent_id, project_id, goal_id, parent_issue_id, priority, status, concurrency_policy, catch_up_policy.
GET /api/routines/{routineId} Routine with triggers (detail).
PATCH /api/routines/{routineId} Same fields as create (all optional).
DELETE /api/routines/{routineId} Delete.
POST /api/routines/{routineId}/run Fire the routine immediately (manual run).
GET /api/routines/{routineId}/runs Run history.
POST /api/routines/{routineId}/triggers Create a trigger (see below).

Create-trigger body: kindschedule/webhook (required), label?, enabled? (default true), and:

  • schedulecron_expression (required, validated), timezone?.
  • webhook → optional signing_mode (none/hmac/hmac_ts). When hmac or hmac_ts, the response includes a one-time secret, and the public fire endpoint requires the X-Staple-Signature HMAC — plus a fresh X-Staple-Timestamp for hmac_ts (see Webhook ingress). Creating a signed trigger with TRIMUS_ENCRYPTION_KEY unset → 503.

Routine object: id, company_id, title, description?, assignee_agent_id?, project_id?, goal_id?, parent_issue_id?, priority, status, concurrency_policy, catch_up_policy, created_at, updated_at. Concurrency policies: always_enqueue, coalesce_if_active, skip_if_active. Catch-up policies: skip_missed, enqueue_missed_with_cap.

Webhook triggers (rotate/update)

Trigger-scoped management (bearer; company resolved on the trigger's routine).

Method Path Purpose
PATCH /api/routine-triggers/{triggerId} Update a trigger (label/enabled/cron/timezone/signing-mode).
DELETE /api/routine-triggers/{triggerId} Delete a trigger.
POST /api/routine-triggers/{triggerId}/rotate-secret Mint a new HMAC signing secret (returned once). 503 if encryption unconfigured.
POST /api/routine-triggers/{triggerId}/rotate-url Regenerate the trigger's public fire URL (public_id) after a leak; the old fire URL stops working immediately.

Trigger object: id, routine_id, kind, label?, enabled, cron_expression?, timezone?, next_run_at?, public_id?, signing_mode?, replay_window_sec?, created_at, updated_at (the secret hash is never serialized).

Notes

Notes carry kinds (user_authored/agent_authored/auto_extracted) and visibility scoping (user notes private; agent notes company-wide), enforced in the handlers. List/create are company-scoped; per-note routes self-check.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/notes viewer+ List (visibility-scoped to the caller).
POST /api/companies/{companyId}/notes member+ Create. Body: title (required), body_markdown, labels[]. 201.
POST /api/companies/{companyId}/notes/search viewer+ Semantic search. Body: {query, limit?} (default 5, cap 100). Returns {notes:[…]} ranked by cosine similarity. 502 if embedding call fails, 503 if no embedding-capable provider.
GET /api/notes/{noteId} bearer One note.
PATCH /api/notes/{noteId} bearer Body: title, body_markdown, labels (optional; empty title → 400).
DELETE /api/notes/{noteId} bearer Delete.
POST /api/notes/{noteId}/archive · /unarchive bearer Archive / restore.
POST /api/notes/{noteId}/promote bearer Promote note → issue. Body (optional): status (default backlog), priority (default medium), assignee_agent_id?, project_id?. Returns {issue, note}; the note is archived. 201.

Note object: id, company_id, author_user_id?, author_agent_id?, title, body_markdown, labels[], kind, tags[], created_at, updated_at, archived_at?.

Knowledge Base

Versioned, company-scoped knowledge documents the engine injects into agent prompts. Reads are Viewer+. Writes are Member+ and human-only — the handlers additionally require a logged-in user actor and reject agent / board-user keys with 403 (agents consume knowledge; they don't author it via this API). DELETE soft-archives (matching notes); rollback restores a prior version's content as a new version. Per-doc cross-company access returns 404.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/knowledge viewer+ List. Query: category, status, inject_mode, tag (repeatable), include_archived=true. Returns {"docs":[…]}.
GET /api/companies/{companyId}/knowledge/{docId} viewer+ One document.
GET /api/companies/{companyId}/knowledge/{docId}/versions viewer+ Version history (up to 200 entries). Returns {"versions":[…]}.
GET /api/companies/{companyId}/knowledge/{docId}/versions/{version} viewer+ One historical version. 404 if the version doesn't exist.
POST /api/companies/{companyId}/knowledge member+ (human) Create. Body: title (required), body_markdown, category, tags[], inject_mode, inject_tags[], source_ref. Invalid inject_mode400. 201.
PATCH /api/companies/{companyId}/knowledge/{docId} member+ (human) Update. All fields optional: title, body_markdown, category, tags, inject_mode, inject_tags, status, source_ref (empty title or invalid enum → 400).
DELETE /api/companies/{companyId}/knowledge/{docId} member+ (human) Soft-archive. Returns {"status":"archived"}.
POST /api/companies/{companyId}/knowledge/{docId}/rollback/{version} member+ (human) Roll back: creates a new version carrying the target version's content. 404 if the version doesn't exist.

Valid inject_mode: always, relevant, tagged. Valid status: active, archived, draft. Knowledge object: id, company_id, title, body_markdown, category, tags[], inject_mode, inject_tags[], source, source_ref?, version, last_edited_by_user_id?, last_edited_by_agent_id?, status, archived_at?, created_at, updated_at, project_id? (project_id set = a project-scoped doc).

Git-sync configuration and manual sync (/companies/{companyId}/knowledge/git-sync[/run]), the Slack-channel ingestion allowlist, and Compiled Knowledge management are UI-only admin surfaces — see the callout under Companies for the instance-admin routes.

Skills (DB / instance)

Two systems coexist. DB skills are JSON-defined, company-scoped, and materialized to disk on write. Instance skills are global, instance-admin managed.

Company DB skills

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/skills viewer+ List.
POST /api/companies/{companyId}/skills member+ Create. Body: name (required), description?, definition (JSON).
GET /api/skills/{id} bearer One skill.
PATCH /api/skills/{id} bearer Body: name?, description?, definition.
DELETE /api/skills/{id} bearer Delete.

Company-skill object: id, company_id, name, description?, definition, created_by_user_id?, created_by_agent_id?, created_at, updated_at.

Instance skills (/api/admin/skills)

All gated by RequireInstanceAdmin.

Method Path Purpose
GET / POST /api/admin/skills List / create. Create body: name (required), description?, definition (valid JSON).
GET / PATCH / DELETE /api/admin/skills/{skillId} Get / update (name?, description?, definition) / delete.

Instance-skill object: id, name, description?, definition, created_by_user_id?, builtin, created_at, updated_at.

Filesystem skills

YAML-defined, slug-addressed skills materialized into the agent CWD. Company- and agent-scoped variants share handlers. List/create are Member+; reads Viewer+.

Method Path Purpose
GET /api/companies/{companyId}/fs-skills List skills (company scope).
POST /api/companies/{companyId}/fs-skills Create. Body: slug (required, validated), content, name, description, tags[]. 201.
GET /api/companies/{companyId}/fs-skills/{slug} One skill.
PUT /api/companies/{companyId}/fs-skills/{slug} Update. Body: content, name, description, tags[], expected_mtime? (optimistic-concurrency). Returns the skill + optional warning.
DELETE /api/companies/{companyId}/fs-skills/{slug} Delete.
(same set) /api/agents/{agentId}/fs-skills[/{slug}] Agent-scoped equivalents.

FS-skill object: slug, content, meta{name, description, enabled?, created_by, tags[]}, mod_time, shadowed?, shadowed_by?.

Files (company- and agent-scoped)

Per-scope file storage — nested subdirectory paths are supported and listed recursively (#365), distinct from project workspace files. Reads Viewer+; writes Member+.

Method Path Purpose
GET /api/companies/{companyId}/files List [{name, size, modified_at}].
POST /api/companies/{companyId}/files Multipart upload (file). 409 if the name exists; 413 if too large. 201{name, size, modified_at}.
GET /api/companies/{companyId}/files/* Download (nested paths supported, #365).
DELETE /api/companies/{companyId}/files/{filename} Delete.
(same set) /api/agents/{agentId}/files (/* to download, /{filename} to delete) Agent-scoped equivalents.

Company home directory: GET /api/companies/{companyId}/home{home_dir, is_override} (viewer+).


Chats

Operator↔agent conversations. List/create are company-scoped (read Viewer+, create Member+). Per-chat routes carry no companyId; access is checked inside each handler against the loaded chat (403 on mismatch, not 404).

Method Path Purpose · key fields
GET /api/companies/{companyId}/chats viewer+ — list.
POST /api/companies/{companyId}/chats member+ — create. Body (all optional): title, current_agent_id, metadata (JSON). A bare POST opens an empty chat.
GET /api/chats/{chatId} One chat.
PATCH /api/chats/{chatId} Body: title?, current_agent_id (string or null to detach), metadata.
DELETE /api/chats/{chatId} Delete.
GET /api/chats/{chatId}/messages List messages.
POST /api/chats/{chatId}/messages Send. Body: content_markdown (required). Calls the chat's agent and returns {user_message, agent_message?}. 422 if the agent has no provider; 502 on LLM failure.
POST /api/chats/{chatId}/messages/stream Same, token-streamed as text/event-stream (events: chunk, done, error).
POST /api/chats/{chatId}/archive · /unarchive Archive / restore.
POST /api/chats/{chatId}/escalate Write an escalation system marker and best-effort DM paired instance admins.
POST /api/chats/{chatId}/compress Force context compression now (bypasses the enable flag). Returns {compressed_runs, tokens_before, tokens_after, tokens_saved, model, provider, compression_id}. 400 if the chat has no agent.

Chat object: id, company_id, created_by_user_id?, current_agent_id?, title, metadata, parent_chat_id?, created_at, updated_at, archived_at?. Message object: id, chat_id, seq, role, author_user_id?, author_agent_id?, content_markdown, tokens_in?, tokens_out?, cost_usd?, model?, provider?, metadata, created_at.

Feedback (chat-message + issue-comment thumbs) lives in the session JSON-API group (cookie or bearer), CSRF-protected: POST/DELETE /api/chat-messages/{messageId}/feedback and POST/DELETE /api/issue-comments/{commentId}/feedback.

Providers

Per-company LLM provider config; api_key + custom_headers are encrypted at rest. List/create are admin; per-provider GET/models are open to the company, writes are admin.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/providers admin List.
POST /api/companies/{companyId}/providers admin Create. Body: label (required), adapter_type (required — see below), api_key (required), base_url?, custom_headers (JSON, validated). 503 if encryption unconfigured.
GET /api/providers/{providerId} bearer One provider.
GET /api/providers/{providerId}/models bearer Available models for the provider.
PATCH /api/providers/{providerId} admin Body: label?, api_key?, base_url?, custom_headers.
PATCH /api/providers/{providerId}/enabled admin Body: {"enabled":bool}.
POST /api/providers/{providerId}/test admin Live connection test.
DELETE /api/providers/{providerId} admin Delete.

Valid adapter_type (provider family): anthropic, openai_compatible, perplexity, bedrock. (OpenAI is configured as openai_compatible.) Provider object: id, company_id, label, adapter_type, api_key, base_url?, enabled, custom_headers, needs_rotation?, created_at, updated_at. There is also GET /api/companies/{companyId}/secret-providers (viewer+) returning the distinct provider names referenced by secrets.

Secrets

Per-company secrets, encrypted at rest, with an audited reveal path (SEC-017). List/create/audit are admin; reveal self-gates in-handler (denied attempts still write an audit row).

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/secrets viewer+ List metadata.
POST /api/companies/{companyId}/secrets admin Create. Body: name (required), provider, description?, external_ref?, encrypted_value (required — the plaintext to encrypt). 503 if encryption unconfigured.
GET /api/companies/{companyId}/secrets/audit admin Reveal/deny audit log.
GET /api/secrets/{id} bearer Metadata only (never the value).
POST /api/secrets/{id}/reveal bearer, company/instance admin (in-handler) Reveal plaintext. Body: reason?. Audited; non-admin → 403 + denied audit row. Returns {id, name, value, version_number}.
PATCH /api/secrets/{id} admin Body: description?, external_ref?.
POST /api/secrets/{id}/rotate admin Body: encrypted_value (required). New version; 503 if encryption unconfigured.
DELETE /api/secrets/{id} admin Delete.

Secret object: id, company_id, name, provider, description?, external_ref?, created_by_user_id?, needs_rotation?, created_at, updated_at.

Plugins

Sandboxed plugin host. Reads are open to the company; lifecycle ops are admin; approval transitions (allow/ban/rescind-ban) self-gate in-handler so denied attempts audit before 403.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/plugins viewer+ List plugins.
POST /api/companies/{companyId}/plugins admin Create. Body: name, slug (required), description, version, binary_path, binary_hash, config (JSON).
GET /api/companies/{companyId}/plugins/tools viewer+ All plugin tools in the company.
GET /api/companies/{companyId}/plugins/audit admin Approval-transition audit log.
POST /api/companies/{companyId}/plugins/tools/invoke member+ Invoke any tool by namespace:name. Body: tool, input (JSON).
POST /api/companies/{companyId}/plugins/webhooks/{path} member+ Deliver an inbound webhook to a plugin (not public).
GET /api/plugins/{pluginId} bearer One plugin.
GET /api/plugins/{pluginId}/health · /tools · /events · /jobs · /webhooks bearer Plugin introspection.
POST /api/plugins/{pluginId}/allow · /ban · /rescind-ban bearer (admin in-handler) Approval state machine; allow pins the binary SHA-256.
PUT / DELETE /api/plugins/{pluginId} admin Update / delete. Update body: name?, description?, version?, binary_path?, binary_hash?, config?, enabled?.
POST /api/plugins/{pluginId}/start · /stop · /restart admin Lifecycle.
POST /api/plugins/{pluginId}/tools/invoke admin Invoke a tool on this specific plugin. Body: tool, input.

Plugin object (selected): id, company_id, name, slug, description?, version, binary_path?, binary_hash?, state, config, manifest, error_message?, enabled, last_health_check?, started_at?, stopped_at?, approval_state, approved_binary_hash?, approved_by_user_id?, approved_at?, banned_by_user_id?, banned_at?, ban_reason?, host_capabilities, created_at, updated_at. See PLUGIN.md.

Costs, budgets & feedback

Cost reporting, budget policies/incidents, monthly LLM budgets, and feedback export. Reads Viewer+; cost-event create Member+; policy create + feedback export admin.

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/costs viewer+ Aggregated cost summary. Query: groupByagent/model/day/billing_code, since.
POST /api/companies/{companyId}/cost-events member+ Report a cost event (then evaluates budget policies). Body: agent_id?, heartbeat_run_id?, provider?, model?, billing_code?, biller?, billing_type?, input_tokens, cached_input_tokens, output_tokens, cost_cents, occurred_at?, metadata?.
GET /api/companies/{companyId}/budget-policies viewer+ List policies.
POST /api/companies/{companyId}/budget-policies admin Create. Body: scope_type, scope_id?, metric, window_kind, window_size, threshold_type, threshold_value (> 0).
PATCH /api/companies/{companyId}/budget-policies/{policyId} admin Update a policy.
DELETE /api/companies/{companyId}/budget-policies/{policyId} admin Delete a policy.
GET /api/companies/{companyId}/budget-incidents viewer+ List incidents.
POST /api/budget-incidents/{incidentId}/resolve bearer, admin Body: resolution_action.
GET /api/companies/{companyId}/budget/status viewer+ Monthly LLM budget vs month-to-date spend. {configured, monthly_usd, spent_usd, spent_pct, warning_pct, critical_pct, status} or {configured:false}.
GET /api/companies/{companyId}/feedback viewer+ Feedback votes.
POST /api/companies/{companyId}/feedback/export admin Kick off a feedback export.
GET /api/companies/{companyId}/feedback/exports viewer+ List exports.
GET /api/feedback-exports/{id} bearer Download one export.
POST /api/issues/{issueId}/feedback bearer Cast a feedback vote on an issue.

Cost-event object: id, company_id, agent_id?, heartbeat_run_id?, provider?, model?, billing_code?, biller?, billing_type?, input_tokens, cached_input_tokens, output_tokens, cost_cents, occurred_at, metadata?. Budget-policy object: id, company_id, scope_type, scope_id?, metric, window_kind, window_size, threshold_type, threshold_value, status, created_at, updated_at. Budget-incident object: id, company_id, policy_id, status, opened_at, resolved_at?, resolution_action?, details?.

Portability (export / import)

Company export/import. Export is admin (company-scoped); import is instance-admin.

Method Path Role Purpose
POST /api/companies/{companyId}/portability/export-preview admin Preview an export.
POST /api/companies/{companyId}/portability/export admin Export the company (JSON download).
POST /api/portability/import-preview instance-admin Preview an import incl. provider preflight.
POST /api/portability/import instance-admin Import a company bundle.
POST /api/companies/{companyId}/bulk-import instance-admin or company-admin (in-handler) Bulk-import context capabilities (currently memory-kind only) into the company. A non-instance-admin caller must be admin of the same company (mismatch → 404). Lives in the session/CSRF-protected group, so browser-session callers need a CSRF token.

Company-scoped reads (Viewer+) plus asset/logo management.

Method Path Role Purpose
GET /api/companies/{companyId}/dashboard viewer+ Dashboard aggregates.
GET /api/companies/{companyId}/org-chart.svg · .png viewer+ Rendered org chart.
GET /api/companies/{companyId}/assets viewer+ List assets.
POST /api/companies/{companyId}/assets member+ Multipart asset upload.
GET /api/assets/{id}/download bearer Download an asset.
DELETE /api/assets/{id} bearer Delete an asset.
GET /api/companies/{companyId}/logo viewer+ Logo metadata.
GET /api/companies/{companyId}/logo/image viewer+ Logo bytes (1 h cache; 404 if none).
POST / DELETE /api/companies/{companyId}/logo admin Upload (multipart) / delete the logo.

The activity feed (/companies/{id}/activity) and capability registry (/companies/{id}/capabilities) are UI routes; activity rows are written as a side effect of issue mutations rather than via a dedicated JSON endpoint.

Heartbeat runs & event replay

Streaming-log replay / gap recovery for a run's event transcript (bearer).

Method Path Purpose
GET /api/heartbeat-runs/{runId}/events Run events (legacy path).
GET /api/runs/{runId}/events Run events (replay / gap recovery).

Run-event object: id, run_id, seq, kind, payload, created_at.

User-scoped UI ordering, readable/writable by any authenticated actor.

Method Path Purpose
GET / PUT /api/sidebar-preferences/companies Get / set the company ordering.
GET /api/companies/{companyId}/sidebar-preferences/sections Get section ordering (viewer+).
PUT /api/companies/{companyId}/sidebar-preferences/sections Set section ordering (member+).

Live updates (SSE)

Method Path Auth Purpose
GET /api/live/events session or bearer Server-Sent Events stream of company-scoped board events.

Event types include issue_updated, activity_created, approval_created, approval_resolved, chat_created, chat_message_created, chat_updated, note_created, note_promoted, agent_status_changed, heartbeat_started, and run.started. Membership is periodically re-verified (SEC-030): live events stop for users whose company membership is revoked mid-connection. Diagnostics: GET /api/admin/sse-metrics (instance-admin).

MCP server endpoint

Trimus exposes its own Model Context Protocol JSON-RPC endpoint backed by the per-company typed-tools registry.

Method Path Auth Purpose
POST /mcp/{companyId} bearer JSON-RPC 2.0. The handler enforces the actor's company matches {companyId}; cross-tenant → 404.

Supported methods: initialize, notifications/initialized (sink), tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get. tools/call returns a single text content block carrying the tool's JSON output verbatim; tool-not-found and tool errors come back as an isError content result (not a JSON-RPC error). jsonrpc != "2.0" → invalid- request error.

A2A federation

Agent-to-Agent federation. Public discovery cards are in Agent-card discovery; the inbound JSON-RPC endpoint and outbound peer management require a bearer key.

Inbound (per agent)

Method Path Auth Purpose
POST /companies/{companyId}/agents/{agentId}/a2a bearer A2A v1.0 JSON-RPC. The handler verifies the resolved actor is the named agent and that the agent is externally-available/published; mismatches → 404 (no existence leak).

Methods: message/send, tasks/get, tasks/cancel, message/stream, tasks/pushNotificationConfig/{set,get,list,delete}.

Outbound peers

Method Path Role Purpose · key fields
GET /api/companies/{companyId}/a2a/peers bearer (company) List peers.
POST /api/companies/{companyId}/a2a/peers bearer (admin via group) Register. Body: label, url (required), api_key? (encrypted at rest; 503 if encryption unconfigured), timeout_seconds?, rate_limit_rpm?, refresh_interval_seconds?, local_preference_bias?.
GET /api/a2a/peers/{peerId} bearer One peer.
PATCH /api/a2a/peers/{peerId} bearer Update peer fields.
DELETE /api/a2a/peers/{peerId} bearer Delete.
POST /api/a2a/peers/{peerId}/refresh bearer Re-fetch the peer's agent-card directory now.

Peer object: id, company_id, label, url, timeout_seconds, rate_limit_rpm, refresh_interval_seconds, local_preference_bias, last_fetched_at?, last_fetch_error?, status, enabled, created_at, updated_at (the encrypted API key is never serialized).

Remote workers

Bootstrap (register) is public — see Remote-worker bootstrap; everything else, including operating-prompt, uses the worker key (WorkerAuth). The admin list (GET /api/workers) is bearer-only (agent or board-user API key); session cookies are not accepted — the browser equivalent is the /admin/workers UI page (instance-admin, session-authed).

Method Path Auth Purpose · key fields
GET /api/workers/operating-prompt worker key Re-fetch the worker operating contract. Requires a valid, approved worker key (Authorization: Bearer sk_…); anonymous callers get 401. The operating prompt is delivered in the register response — this endpoint is only for an already-onboarded worker to re-fetch it after a server upgrade.
POST /api/workers/heartbeat worker key Worker liveness ping.
POST /api/workers/tasks/claim worker key Claim a task. Body (optional): tags[]. Tags are server-authoritative (#336) — the worker's stored set is the ceiling, so a supplied tags[] may only narrow to a subset (intersection), never broaden the worker past its registered tags; omit it to claim with the full stored set. 200 → task, or 204 when none available.
POST /api/workers/tasks/{taskId}/complete worker key Complete. Body: the task result as raw JSON. 204.
POST /api/workers/tasks/{taskId}/fail worker key Fail. Body: error. 204.
GET /api/workers/tasks/{taskId}/control worker key Poll for a cancel signal mid-execution. Returns {"cancel":bool}.
POST /api/workers/tasks/{taskId}/events worker key Forward subprocess engine events so they land in the same run transcript + SSE channel as in-process runs.
GET /api/workers bearer List registered workers.

Worker object: id, name, tags[], status, last_seen_at?, created_at. Task object: id, heartbeat_run_id, agent_id, company_id, worker_id?, tags_required[], payload, status, claimed_at?, completed_at?, result, error?, created_at. Full protocol: REMOTE_WORKERS.md.

A worker's tags are server-authoritative and editable by an instance admin on /admin/workers (#336, session-authed UI action — not part of these tables). An idempotent register-resume does not adopt the resume body's tags; it echoes the stored authoritative tags in the response so a worker with drifted env tags can reconcile.

Instance administration

Instance-wide settings, feature flags, and instance roles — all RequireInstanceAdmin. (The many /admin/* HTML dashboards — operations, audit, MCP calls, subagents, GEPA, costs, user-models — are UI routes.)

In-app license install/remove is UI-only: POST /admin/operations/license and POST /admin/operations/license/remove (instance-admin, session). That group is deliberately not license-gated, so a degraded (read-only) instance can self-heal without a restart.

Method Path Purpose · key fields
GET /api/instance-settings List instance settings (open to any authenticated reader).
PATCH /api/instance-settings Upsert. Body: a { "<key>": <json-value>, … } map.
GET /api/admin/flags List operational flags + effective values.
GET /api/admin/flags/{name} One flag.
PUT /api/admin/flags/{name} Upsert an override. Body: value (required; empty → use DELETE), description?. Writes the same audit row the UI panel does.
DELETE /api/admin/flags/{name} Remove an override (revert to default).
GET /api/instance/roles List users' instance roles.
PATCH /api/instance/roles/{userId} Set a user's instance role. Body: roleadmin/member.
GET /api/admin/sse-metrics SSE hub diagnostics (per-company publish/sent/drop counters).
GET /api/admin/workers/queue-stats Remote-worker fleet queue statistics (instance-admin; session or bearer).

Trash

Soft-delete restore/purge for skills/home/entities — RequireInstanceAdmin.

Method Path Purpose
GET /api/admin/trash List trashed entries.
POST /api/admin/trash/{type}/{entityId}/{trashId}/restore Restore. 200 {"status":"restored"}; 409 on conflict.
DELETE /api/admin/trash/{type}/{entityId}/{trashId} Hard-delete (purge).

LLM configuration reflection

Plain-text (text/plain) self-describing docs any authenticated caller (agents included) can fetch to learn the agent-configuration surface.

Method Path Purpose
GET /llms/agent-configuration.txt The full agent-configuration reference.
GET /llms/agent-configuration/{adapterType}.txt Per-adapter configuration reference.
GET /llms/agent-icons.txt The agent-icon catalog.

Agent action verbs (out-of-band)

Agents do not drive most of the API directly during a run. Instead they emit slash-command action verbs in their LLM output, which the engine (internal/engine/actions.go) parses and applies against the same data model documented above. Both kebab-case and snake_case are accepted (/create-issue == /create_issue).

Verbs span issue control (/status, /priority, /assign, /checkout, /release), creation/reads (/create-issue, /get-issue, /list-issues), relations (/blocks, /blocked-by, /relates), approvals (/request-approval), notes (/create-note, /edit-note, /get-note, /list-notes, /archive-note, /unarchive-note, /record-finding), install/capability verbs (/install-memory, /install-note, /install-skill, /use-skill, /install-routine, /install-agent, /install-federation), routines (/fire-routine, /promote-routine), subagents (/spawn-subagent, /await-subagents, /await-subagents-recursive), routing/flow (/needs-routing, /await-input), profile (/update-user-model), reporting (/report, alias /write-report — creates a report work product on the current issue; heartbeat-only), and workspace (/write-file).

These are an in-band execution protocol, not HTTP endpoints — they are listed here only to clarify how agents mutate the resources above. The authoritative verb set lives in actions.go.