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
- Authentication
- Errors
- IDs, timestamps, pagination
- Rate limits & body size
- Cross-company isolation
- Route groups & middleware
- Public endpoints (no auth)
- Health & metrics
- Authentication & OAuth
- Agent-card discovery (A2A)
- Remote-worker bootstrap
- Webhook ingress
- Role default prompts
- Identity & profile
- Users (instance-admin)
- Companies
- Company members, invites & join requests
- Agents
- Agent self-service (
/api/agents/me) - Per-company agent defaults
- Issues
- Comments
- Documents, attachments, work products, relations
- Issue inbox & read state
- Approvals
- Projects & project workspace files
- Goals
- Routines & triggers
- Webhook triggers (rotate/update)
- Notes
- Knowledge Base
- Skills (DB / instance)
- Filesystem skills
- Files (company- and agent-scoped)
- Chats
- Providers
- Secrets
- Plugins
- Costs, budgets & feedback
- Portability (export / import)
- Activity, dashboard, assets & logo
- Heartbeat runs & event replay
- Sidebar preferences
- Live updates (SSE)
- MCP server endpoint
- A2A federation
- Remote workers
- Instance administration
- Trash
- LLM configuration reflection
- Agent action verbs (out-of-band)
Conventions¶
Base URL¶
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:
| 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 withlimit(default 50) and an opaquecursorquery param — notoffset. - 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_RPMrequests/minute per limiter (default 300), applied to the public, SSE, user-session, and bearer API groups. Over-limit →429. Client IP is taken fromX-Forwarded-Foronly when the peer is inTRIMUS_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 — seeTRIMUS_ATTACHMENT_*andTRIMUS_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 responding403.
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, applyRequireCompanyAccess, and a role gate (Viewer+/Member+/Admin). A{companyId}that isn't yours → 404. - Resource-scoped routes (
/api/issues/{id},/api/chats/{id}, …) carry nocompanyId, 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 viaPATCH /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_state → 400. |
| 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_type ∈ blocks/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_type ∈ auto_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: status ∈ approved/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: kind ∈ schedule/webhook (required), label?,
enabled? (default true), and:
schedule→cron_expression(required, validated),timezone?.webhook→ optionalsigning_mode(none/hmac/hmac_ts). Whenhmacorhmac_ts, the response includes a one-timesecret, and the public fire endpoint requires theX-Staple-SignatureHMAC — plus a freshX-Staple-Timestampforhmac_ts(see Webhook ingress). Creating a signed trigger withTRIMUS_ENCRYPTION_KEYunset →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_mode → 400. 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}/feedbackandPOST/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: groupBy ∈ agent/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. |
Activity, dashboard, assets & logo¶
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.
Sidebar preferences¶
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/licenseandPOST /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: role ∈ admin/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.