All
/api/v1/agents/* endpoints require authentication and workspace context. Workspace context may be supplied as workspace_id, a {workspaceId} path value, or the X-Workspace-ID header; the chat-message proxy (/api/v1/chats/{chatId}/messages) is the exception — it resolves the workspace from the chat row and takes no workspace context parameter.Endpoints
Status & Metrics
Lightweight, read-only aggregates that power toolbar and dashboard widgets.Crews Status
X-Workspace-ID (or the
equivalent workspace context accepted by the API).
Response: 200 OK
400 for invalid workspace context, 401 when unauthenticated,
403 when the caller cannot access the workspace, and 500 when the status
query fails.
Contract: GET /api/v1/agents/crews-status
Auth: Authenticated request with workspace context. Request: no body;
the workspace is supplied through the workspace context/header. Response:
200 OK with the status-count object above. Status: 400, 401, 403,
or 500 as described above.
The agent status buckets (total, running, error, idle) are always present — zero counts are not omitted. Any status that is not RUNNING or ERROR (including IDLE and PENDING_REVIEW) folds into the idle bucket; there is no stopped bucket. The queued field counts admission-queue assignments (not agents) currently in the QUEUED state — independent of the agent buckets, and 0 on servers that pre-date the queue migration (internal/api/agents.go:111-151).
Agent Load
X-Workspace-ID (or the
equivalent workspace context accepted by the API).
Response: 200 OK — an array (never null) of entries with agent_id,
agent_name, agent_slug, agent_status, active_tasks, pending_tasks,
completed_today, tokens_used_today, and token_budget. Active tasks include
IN_PROGRESS; pending tasks include PENDING and BLOCKED; token usage and
completed counts are calculated over the current UTC day/24-hour reporting
window used by the handler.
Status: 400 for invalid workspace context, 401 when unauthenticated,
and 403 when the caller cannot access the workspace.
Model Discovery
Enumerate the models an LLM provider can serve, so an operator (or an agent building aPOST/PATCH /api/v1/agents request) doesn’t have to guess a model identifier. Discovery is live when possible, curated when not (internal/api/models.go:84): if the workspace has an active API_KEY credential for the provider, Crewship queries the provider’s own models API; otherwise it returns a hand-maintained curated fallback. The source field reports which path was taken. See the Model discovery guide for the CLI equivalent (crewship model list) and the llm_model validation this same resolver drives on agent update.
List Models
authed + workspace context; the active provider credential is read from the resolved workspace) (internal/api/router_crews.go:230).
Query Parameters:
Response:
200 OK
OLLAMA has no curated fallback. Its model set is whatever the local daemon has pulled, so there is no sensible static list. When the daemon can’t be reached the endpoint returns 502 rather than an empty list, so “daemon unreachable” is never mistaken for “no models installed” (internal/api/models.go:103-107). GOOGLE has no live lister yet and always resolves to its curated set (internal/api/models.go:78).Lifecycle & CRUD
Create, read, update, and soft-delete permanent agents.List Agents
Response:
200 OK
mcp_config_json and created_by_user_id carry omitempty — they are dropped from the JSON entirely when null/empty rather than serialized as null.
Response Fields
Create Agent
OWNER, ADMIN, or MANAGER role
Request Body:
CLI Adapters:
CLAUDE_CODE, CODEX_CLI, GEMINI_CLI, OPENCODE, CURSOR_CLI, FACTORY_DROID
Tool Profiles: MINIMAL, CODING, FULL
Response: 201 Created — returns the created agent object.
Get Agent
200 OK — full agent object.
Update Agent
canEditAgent in internal/api/agents_update.go:27, defined at internal/api/rbac.go:151). OWNER / ADMIN may edit any agent; MANAGER may edit only agents they created or agents in crews where they hold per-crew ADMIN/OWNER; MEMBER / VIEWER are refused.
Request Body: Same fields as Create, all optional. status is not mutable through this endpoint. Additionally supports:
Suggested prompts.
suggested_prompts holds the questions this agent
offers as buttons under an empty conversation, one per line — at most 8 lines,
at most 120 characters each. The server normalises on write: CRLF becomes LF,
each line is trimmed, blank lines are dropped, and a value that ends up empty
is stored as null. An agent with null here is not misconfigured; it simply
shows the built-in defaults, which is what every agent did before the field
existed. Rejections name the offending line — "at most 8 prompts are allowed (got 9)", "prompt 3 exceeds 120 characters (it has 154)" — so a caller can
fix the entry rather than the whole list. Set it from the console under an
agent’s Config -> Chat suggestions, or with
crewship agent update <agent> --suggested-prompts @questions.txt.
Ask forms. ask_forms holds the questionnaires this agent offers: a chip
that opens a short form, whose answers are rendered into an ordinary user
message and sent. The value is a JSON array given as a string — the column
is TEXT, the console edits it in a textarea and the CLI reads it from a file,
and accepting a nested array as well would put two encodings of one value on
one endpoint.
id is a slug and must be unique; label is the chip’s text (max 48
characters); attachment is none | optional | required and defaults to
none; template is at most 2000 characters. Field types are text,
textarea, number, money, date, month, select, multiselect,
checkbox, file and photo — an unrecognised type is accepted and falls
back to a text input, which is what lets a new type ship without a
coordinated frontend release. select and multiselect must carry options.
A JSON Schema for authoring lives at schemas/ask-form.v1.json.
Money fields answer to two placeholders. A money field named amount
renders as {{amount}} for the number and {{amount_currency}} for the
chosen currency. The second name is derived so that two money fields on one
form cannot fight over a single {{currency}}.
Template rendering. {{field}} substitution only — no conditionals, no
loops, no expressions. Three rules decide what gets sent:
- Every placeholder must name a field on its own form, and this is checked when the form is saved, not when it is rendered. The author finds out while authoring; the person talking to the agent never meets a broken template.
- An empty optional value drops the whole line it sits on, static label
and all, unless another placeholder on that line produced something. So a
receipt with no category sends no
Category:line rather than a dangling one. This is the only rule that is not obvious from reading a template. - Values are inserted verbatim — the result is a user message, so nothing is
markdown-escaped — but control characters are stripped, each value is
capped at 2000 characters and the finished message at 32000.
fileandphotovalues render as the agent-visible pathattachments/<chatId>/<name>, one per line when there are several.
form "receipt": template names {{suplier}}, which is not a field on that form, form "receipt": has no fields, two forms share the id "receipt", at most 4 forms are allowed per agent (got 5). Nothing is
written when a definition is refused. A value that normalises to nothing —
"", whitespace, [], or null — clears the column.
The stored value is canonical: parsed, validated and re-encoded with
defaults spelled out and a stable key order, so what a caller reads back is
stable rather than showing a diff nobody made. Set it from the console under
an agent’s Config -> Ask forms, or with crewship agent update <agent> --ask-forms @forms.json, and render one without a browser with crewship agent ask-preview <agent> <form-id> --var k=v.
Response: 200 OK — updated agent object.
Delete Agent
deleted_at).
Request: No body; agentId is a required path parameter and workspace
context scopes the lookup.
Response: 200 OK
Rotate Webhook Secret
200 OK
Hire / Rehire / Approve
Ephemeral agents (“contractors”) are short-lived agents spawned against a crew with a TTL. When the TTL elapses the agent becomes a ghost (the row is preserved for audit but no longer counts against the crew’s quota). The hire flow is gated by the crew’s autonomy policy (strict /
guided / trusted / full) and bounded by the crew’s
max_ephemeral_agents quota (ghosts excluded).
All three endpoints require
OWNER, ADMIN, or MANAGER role (canRole(role, "create")).Hire Agent
Auth:
OWNER, ADMIN, or MANAGER role
Request Body:
201 Created (live) or 202 Accepted (pending review).
Credential assignment
On a successful hire (201/202), Crewship auto-assigns the available
workspace Anthropic credentials (API_KEY / AI_CLI_TOKEN, first-created
wins) to the new agent so it can authenticate on first run. This is
best-effort and runs after the agent row is committed: assignment failures are
journaled as credential.auto_assign_failed and never fail the hire, and a
workspace with no Anthropic credential journals credential.auto_assign_empty.
Assign manually later via POST /api/v1/agents/{agentId}/credentials if needed.
Rehire Agent
expired_at, pushes
expires_at forward by the new TTL, and appends a new reason line to
the hire_reason history. The container is not rebuilt here — the
chatbridge auto-provisions a fresh one on the next message. Rehiring a
still-live ephemeral (extending its TTL before it ghosts) is free and does
not consume an extra quota slot.
Auth: OWNER, ADMIN, or MANAGER role
Request Body:
200 OK — same shape as the Hire response, echoing the
agent’s persisted status (rehire does not change it) and the updated
expires_at / hire_reason.
Approve Hire
PENDING_REVIEW ephemeral to
IDLE (an atomic conditional UPDATE, so two concurrent approvals can’t
both win), resolves the blocking inbox waitpoint addressed to the agent,
and writes an agent.hire_approved audit entry. The chatbridge refuses to
start a PENDING_REVIEW agent, so this is what actually releases the gate.
Auth: OWNER, ADMIN, or MANAGER role
Request Body: empty.
Response: 200 OK
Agent Persona
The persona layer is per-agentPERSONA.md markdown that the
orchestrator injects into every system prompt. Resolution is layered:
the agent layer (if a file exists on disk) wins; otherwise the crew
default layer; otherwise the synthesized default built from the
agent’s role + role title. Direct writes are operator-only; agents
edit only via the suggest endpoint, which the policy resolver
gates per crew-autonomy level (see Autonomy & self-learning).
Get Persona
agentId is required and workspace context scopes the
agent lookup.
Response: 200 OK
Status:
400 for invalid request/workspace context, 401 when
unauthenticated, 403 when access is denied, and 500 when persona storage
cannot be read.
Set Persona
memory_versions so Get Persona History can
replay the chain.
Request:
200 OK
Agents cannot use this endpoint to mutate their own persona —
ActionPersonaDirectWrite is DecisionRejected across every autonomy level in Phase 1. Agent edits flow exclusively through Suggest Persona.Reset Persona
agentId is required.
Response: 204 No Content
Status: 401 unauthenticated, 403 when the caller lacks the mutation
permission, and 500 when the persona cannot be reset.
Get Persona History
memory_versions filtered to this agent’s
PERSONA.md path. Pairs with GET /api/v1/admin/memory/versions/{id}/content for content drill-down — the history endpoint deliberately omits content to keep responses small.
Query Parameters:
Response:
200 OK
parent_sha is omitted on the first version (when the chain starts).
Suggest Persona
ActionPersonaDirectWrite is rejected across every autonomy level —
this endpoint is the only path an agent can use to write a
persona.
Request:
200 OK (proposal queued or applied) / 403 (policy rejected)
There is no
inbox_id in this response — the proposal is recorded as an audit_logs row (persona.suggest_pending), not returned to the caller.
Agent Self-Learning Posture
Per-agent flag controlling whetherALLOW decisions from the Keeper
evaluators auto-apply (enabled = true) or queue for operator
approval in the inbox (enabled = false). Orthogonal to the crew’s
autonomy_level: the per-action policy gate (policy.DecideAction)
is the upstream authority — this flag only decides what happens to
already-ALLOWed decisions. See Autonomy & self-learning.
Get Self-Learning
agentId is a
required path parameter. Status: 400 for invalid request context, 401
when unauthenticated, 403 when access is denied, and 500 for a read
failure.
Response: 200 OK
Set Self-Learning
OWNER / ADMIN (canRole(role, "manage"))
— self-learning weakens the inbox-approval invariant, so the operator
who turns it on must be senior enough to own the consequences.
Request:
Response:
200 OK — same shape as GET, reflecting the new state and the freshly-recorded audit triple.
Agent Inbox Summary
agentId is a
required path parameter and workspace context scopes the lookup.
Response: 200 OK
Agent Peer Cards
Per-(agent, user) markdown notes produced by the PeerCardSync
routine — the agent’s own “what I know about this person” file.
These endpoints are operator-facing: the cards live on disk
under {outputBase}/crews/{crewID}/agents/{slug}/.memory/peers/,
and every read/write/delete writes a peer_card_audit row for GDPR
SAR coverage.
The user-facing surface (view-mine / opt-out / delete-mine) lives
under /api/v1/users/me/peer-cards and shares the same disk + DB
primitives — these agent-flavor endpoints exist so an operator can
clean up cards on behalf of a user (compliance ticket, leaver
workflow) or inspect what an agent has been writing about people.
The {userId} path parameter is the raw user_id; the server
derives the user_slug via memory.UserSlug so the URL stays
debuggable.
List Agent Peers
404.
Auth: Authenticated workspace member.
Request: No body; agentId is required and workspace context scopes the
lookup.
Response: 200 OK
Get Agent Peer Card
read row to
peer_card_audit.
Auth: Authenticated workspace member.
Request: No body; agentId and userId are required path parameters and
workspace context scopes the lookup.
Response: 200 OK
Delete Agent Peer Card
agentId and userId are required path parameters.
Response: 204 No Content
The GDPR cascade endpoint
DELETE /api/v1/admin/users/{userId}/data deletes peer cards across all agents in the workspace for a single user — that’s the right path for a full SAR. This per-agent endpoint is for the narrower “clean up just this agent’s notes about this user” case.Related admin user routes
These workspace-admin routes are listed here because the GDPR cross-reference above uses the same admin-user path prefix.List workspace users
OWNER or ADMIN (manage role).
Request / parameters: No request body. Workspace context is supplied by the
authenticated request; there are no path or query parameters.
Response: 200 OK with a JSON array of workspace members and their user,
workspace, and role fields.
Export one user’s data
OWNER or ADMIN (manage role).
Request / parameters: No request body. userId is the required path
parameter for the data subject; the request is scoped to the authenticated
workspace.
Response: 200 OK with the GDPR export bundle for that user, including
peer cards, memory versions, inbox items, user models, and an audit action_id.
Agent avatars
An agent’s avatar is drawn from itsavatar_seed and avatar_style by a
generator that runs in the browser. That makes the rendered face a
function of the installed generator version: upgrading the avatar library
redraws every existing agent, and the roster your team recognises changes
overnight.
To stop that, an agent can carry a stored render. Once stored, that
exact image is served instead of being redrawn, so the agent’s face
survives generator upgrades.
Both states are normal:
Use the string verbatim. It already carries both
?v= and workspace_id,
the latter because this route resolves its workspace the same way every
other endpoint does, and an <img> tag cannot send the X-Workspace-ID
header. Rebuilding the URL by hand and dropping that parameter yields a
400.
The server never draws an avatar (the generator is JavaScript-only). The
client renders the image and stores it — at creation, or the first time
someone with edit rights views an agent that has none. Agents created
through the CLI or crew templates therefore start with
avatar_url: null
and get filled in later.Get stored avatar
image/svg+xml. Readable by any workspace member. workspace_id
is required — it scopes the lookup to your workspace, so one workspace can
never read another’s avatars. Returns 404
when the agent has no stored render — which is not an error, it just means
the client should generate from the seed.
The ?v= parameter carries a content hash and is ignored by the server; it
exists so the response can be cached immutably and still refresh when the
image actually changes.
Auth: Authenticated workspace member.
Request: No body. agentId is required; v is an optional cache-busting
query value and workspace context is required.
Response: 200 OK with an SVG body (image/svg+xml).
Status: 400 for invalid request/workspace context, 401 when
unauthenticated, 403 when the caller cannot access the workspace, and 404
when the agent or stored avatar is not found.
Responses are served with Content-Security-Policy: default-src 'none'; sandbox and X-Content-Type-Options: nosniff, so the SVG cannot load
subresources or execute script even if opened directly.
Store an avatar
PATCH /api/v1/agents/{agentId}.
Auth: Authenticated caller with the same per-agent edit rights as
PATCH /api/v1/agents/{agentId}.
Request: JSON object with required string field svg.
Response: 200 OK with the stored avatar representation.
The SVG is validated against an allowlist of inert drawing, gradient and
metadata elements. Anything capable of loading or executing something —
<script>, <foreignObject>, <use>, <image>, any href, any on*
handler, any external url(…) — is rejected with 400. Maximum 64 KiB.
Storing is write-once. An agent that already has a stored avatar
returns 409; clear it first to replace it. Without that rule, an endpoint
that fires automatically on page view would double as a way to silently
swap a teammate’s agent portrait.
Clear a stored avatar
204. The agent reverts to generate-from-seed — which also means
the next generator upgrade will change its face again. Returns 404 for an
agent that does not exist in this workspace, and 403 without edit rights.
Auth: Authenticated caller with per-agent edit rights.
Request: No body; agentId is required. Workspace context is resolved by
the authenticated route.
Response: 204 No Content on success.
Status: 401 unauthenticated, 403 insufficient edit rights, and 404
when the agent is not found.
When a stored render is dropped automatically
A stored render depicts one specific(avatar_seed, avatar_style) pair, so
it is discarded whenever something changes what the agent is supposed to
look like:
The comparison is on values, not on which keys the request contains.
Clients that resubmit the whole agent on every save — the dashboard’s
settings page does — would otherwise discard the stored render on every
unrelated edit.
Agent Skills
Assign, list, and remove the skills available to an agent.List Skills
200 OK — array of skill assignment objects.
Add Skill
OWNER, ADMIN, or MANAGER role (canRole(role, "create"))
Request Body:
201 Created — { "id": "<assignment_id>" } on a fresh assign.
The assign is idempotent: re-assigning an already-installed skill returns 200 OK with { "id": "<existing_id>", "already_assigned": true } rather than a 409.
Remove Skill
204 No Content
Agent Credentials
Assign, list, and remove the credentials injected into an agent’s container.List Credentials
200 OK — array of the credentials this agent receives.
This answers “what does this agent get?”, not “what rows exist in the assignment table”. A credential linked to the agent’s crew is delivered to that agent and therefore appears here, even though no assignment row exists for it.
An explicit assignment always wins over a crew link for the same credential: it appears once, as
explicit, carrying the env var name, priority and lease that were chosen for it.
Each object also carries the grant’s lease state (see
Short-lived leases). Note that lease_source and grant_source answer different questions — the first says why a grant expires, the second says where it came from:
Assign Credential
OWNER or ADMIN role (canRole(role, "manage") in internal/api/agent_credentials.go:89)
Request Body:
201 Created
Remove Credential
agent_credentials.id), not the credential id.
Auth: OWNER or ADMIN role (canRole(role, "manage") in internal/api/agent_credentials.go:146)
Response: 200 OK
Agent Chats & Runs
List and create interactive chat sessions, and review execution history.List Chats
200 OK — array of chat session objects.
Auth: Authenticated workspace member.
Request: No body; agentId is required and workspace context scopes the
list.
Status: 400 for invalid request/workspace context, 401 when
unauthenticated, and 403 when the caller cannot access the workspace.
Create Chat
201 Created
The optional request body is { "session_id": "…", "origin": "UI" }.
origin may be UI, CLI, WEBHOOK, CRON, or AGENT; unknown values are
stored as empty. The response is { "id": "<chatId>" }.
Auth: Authenticated caller permitted to create a chat for the agent.
Request: Optional JSON object with session_id and origin. origin is
one of UI, CLI, WEBHOOK, CRON, or AGENT.
Status: 200 or 201 with the created chat id; 400 invalid JSON or
origin, 401 unauthenticated, 403 denied, 404 agent not found, and 409
when the requested session conflicts.
Mark Chat Read
200 OK with { "success": true }; a chat that is
not nested under the supplied agent or workspace returns 404.
Auth: Authenticated chat participant/caller with access to the chat.
Request: Empty JSON object ({}); agentId and chatId are required
path parameters.
Response: 200 OK with the chat read-cursor payload (chat_id and
last_read_at).
Status: 401 unauthenticated, 403 denied, and 404 when the chat is
not nested under the supplied agent and workspace.
Rename Chat
chats.title — the name the session list, the chat sidebar and the
agent-replied notification display. A session with no title reads “Untitled
session” everywhere, so this is what makes a list of conversations navigable.
Auth: The chat creator may rename their own chat; an agent editor may rename
any chat for that agent — the same gate as Delete Chat.
Request: { "title": "Refactor the queue worker" }. title is required and
must be a string; an absent key, null, a non-string and a title that is empty
after normalisation are all 400.
Normalisation (server-side, before storage): whitespace runs — including
newlines and tabs — collapse to a single space, because a title is one line by
definition; control characters and invisible formatting characters (zero-width
padding, the right-to-left override used for label spoofing) are stripped, with
the ZERO WIDTH JOINER exempted so emoji sequences survive; the result is trimmed
and capped at 200 characters. The cap counts characters, not bytes, so a
Czech or Japanese title gets the same length as an English one. Over the cap is
400, never a silent truncation.
The stored title is text, not HTML: markup is preserved verbatim (<Draft> plan stays <Draft> plan), and every consumer is responsible for escaping it
when rendering.
Response: 200 OK with the updated chat row — the same object shape one
element of List Chats has, including unread_count, so a client
can replace the row it already holds without refetching the list. Renaming does
not touch last_activity_at: fixing a name must not reorder the list.
Status: 400 invalid or empty/over-long title, 401 unauthenticated, 403
when the caller is neither the creator nor an agent editor, 404 when the chat
is not nested under the supplied agent and workspace (cross-tenant included),
and 500 on a storage failure.
Delete Chat
409). A
successful deletion returns 204 No Content.
Auth: The chat creator may delete their own chat; an agent editor may
delete any chat for that agent.
Request: No body; agentId and chatId are required path parameters.
Response: 204 No Content on deletion (the handler may also return the
documented JSON deletion result).
Status: 401 unauthenticated, 403 denied, 404 for an unknown or
mis-nested chat, 409 when assignments prevent deletion, and 500 for
cleanup failure.
Chat Attachments
file; the agent and chat must belong to the caller’s
workspace. The endpoint returns the sidecar’s attachment metadata and uses
400 for an invalid upload, 404 for an unknown or mis-nested chat, and
502 when the sidecar cannot be reached.
The blob is written to
<crewID>/<agentSlug>/attachments/<chatId>/<attachmentId>/<filename>, which
the agent sees at
/output/<agentSlug>/attachments/<chatId>/<attachmentId>/<filename>. A
provisioned crew owns that tree as uid 1001, so the server cannot create the
directory host-side; the write is replayed through the running crew container
as that uid. A stopped crew therefore returns 409, with the remedy in the
message.
One upload is one identity at one location. The attachment id segment is
what makes that true. Uploading two different files under the same name — say
evidence.pdf twice — produces two attachments, two directories and two
blobs, each resolving to its own recorded checksum; neither overwrites the
other. Re-uploading the same bytes under the same name in the same chat is
a retry, not a second attachment: it resolves to the attachment that already
exists and returns its path.
Chat attachment blobs are deliberately not content-addressed, because the path
is what the agent is told to open. They therefore live outside
<root>/attachments/ and outside the content-addressed reclaim sweep; the
delete endpoint below is how their bytes are recovered.
Metadata is written before the bytes, not after. The row is inserted first
in an unpublished state, the blob is written second, and the row is promoted
last, so 201 means both exist: a success response can never mean “the bytes
landed and nobody recorded it”. Every failure leaves an unpublished row, which
is never listed and never returned, and which a background collector removes
along with any bytes it names once it is more than an hour old (the sweep runs hourly, so within about two hours).
Attachments uploaded before this scheme keep their original two-segment path
(attachments/<chatId>/<filename>) and keep working unchanged — the stored key
is the authority on where an attachment lives, so they list, resolve and delete
exactly like new ones.
Errors from the storage layer are forwarded as a single sentence prefixed with
failed to save attachment: , not as a nested JSON document.
Auth: Authenticated caller with create/write access to the agent chat.
Request: multipart/form-data with required binary file field; no JSON
body.
Response: 201 Created with {filename, size, path, agent_path}. path
is the agent-relative
attachments/<chatId>/<attachmentId>/<filename>; agent_path is the same
location as an absolute in-container path.
Status: 400 invalid upload, 401 unauthenticated, 403 denied or the
chat is not this agent’s, 404 unknown agent/chat, 409 the crew container is
not running (start the crew and retry), 413 too large to write through the
crew container, 503 no container runtime is configured, and 502 sidecar
failure.
List Chat Attachments
200 OK with an array. Each element carries id,
workspace_id, owner_type (chat), owner_id (the chat id), filename,
content_type, size_bytes, sha256, uploaded_by_user_id,
uploaded_by_agent_id, uploaded_by_name, created_at, path and
agent_path. id is the durable identity — it is what the delete below
takes, and it is the segment inside path.
Status: 400 when a path id is missing, 401 unauthenticated, 404 for
an unknown, mis-nested or cross-tenant chat (the three are indistinguishable
by design), 500 on a read failure.
Delete Chat Attachment
204 —
what the caller asked for is already true. Cross-tenant access is refused one
step earlier: an attachment in another workspace is reachable only through a
chat in that workspace, and that chat is a 404 here.
If the unlink fails (most often a stopped crew, which owns the tree) the row is
kept and the error is forwarded as failed to delete attachment: …; retrying
the same call completes the deletion. The order is deliberate — a row that
outlives its bytes is visible and repairable, whereas bytes that outlive their
row are unreachable for ever, since no sweep walks this tree.
Auth: Authenticated caller with create/write access to the agent chat.
Request: No body.
Response: 204 No Content.
Status: 400 when a path id is missing, 401 unauthenticated, 403
denied, 404 for an unknown, mis-nested or cross-tenant chat, 409/5xx
forwarded from the storage layer when the bytes cannot be removed, 502
sidecar failure.
Credential Bindings
id, agent_id,
mcp_server_id, mcp_server_scope (workspace or crew), optional
credential/type/header fields, enabled, optional config_override_json,
timestamps, server names, and optional credential_name. Returns 404 when
the agent is not in the workspace.
Agent Integrations
POST requires mcp_server_id and
mcp_server_scope (workspace or crew); writes require MANAGER+ and reads
require workspace membership. The resolved route returns the effective
workspace → crew → agent integration cascade.
Auth: Authenticated workspace member.
Request: No body; agentId is required and workspace context scopes the
resolution.
Response: 200 OK with an integrations array containing the effective
workspace/crew configuration.
Status: 400 for invalid request/workspace context, 401 when
unauthenticated, 403 when denied, and 404 when the agent is not found.
Agent Notification Channels
{ "channels": [...] }, where each channel contains id, type,
optional provider, and enabled. It is readable by any workspace member;
unknown or cross-workspace agents return 404.
Auth: Any authenticated workspace member.
Request: No body; agentId is required and workspace context is required.
Response: 200 OK with { "channels": [...] }.
Status: 400 when agentId is missing or request context is invalid,
401 when workspace context is missing, 403 when access is denied, and
500 when the channel lookup fails.
Agent Runs
200 OK — array of run objects.
Agent Proxy Endpoints
These endpoints proxy requests through the crewshipd sidecar to the agent’s container.Container Files
/files, which lists the persistent workspace mount). Proxied through crewshipd; the request is rejected before the IPC hop if the agent is not assigned to a crew. (internal/api/proxy_files.go:350)
Auth: Session or CLI token + workspace membership with at least read-tier role (canRole(role, "read"))
Query Parameters:
Response:
200 OK — JSON array of file entries (unwrapped from the IPC layer’s {files: [...]} envelope). Returns [] when the sidecar response is unparseable or empty.
Git Log
slug is forwarded as agent_slug so the sidecar can scope the log to that agent’s directory inside the shared crew container. (internal/api/proxy.go:312)
Auth: Session or CLI token + workspace membership with at least read-tier role (canRole(role, "read"))
Response: 200 OK — JSON array of commit entries (unwrapped from the IPC layer’s {commits: [...]} envelope). Returns [] when the sidecar response is unparseable or empty.
Chat Messages
Related crew routes
This page links the agent lifecycle to the crew that owns it. The canonical crew reference has the full schemas; these endpoint-local notes preserve the request contract for the route references used on this page.Get Crew
crewId is required and workspace_id supplies workspace context.
Response: 200 OK with the full crew object. Status: 404 when the
crew is missing or outside the workspace.
Delete Crew
OWNER or ADMIN role. Request: no
body; crewId is required and workspace_id supplies workspace context.
Response: 200 OK with { "success": true }. Status: 403 for an
insufficient role and 404 when the crew is not found.
?offset= and ?limit= (default 50, max 500).
Auth: Session or CLI token with read-tier role, plus membership of the chat’s workspace. The workspace is resolved from the chat row (no workspace_id query param needed); the caller must appear in workspace_members for that workspace or the call returns 403. (internal/api/proxy.go:258)
Response: 200 OK — proxied message payload. A chat that doesn’t exist yet (new session before its first message) returns { "messages": [] } rather than 404.