Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt

Use this file to discover all available pages before exploring further.

Admin API

Administrative endpoints for workspace management, user listing, and Keeper security audit logs. Most endpoints require the OWNER role.

GET /api/v1/admin/stats

Returns aggregate statistics for the current workspace. Required role: OWNER

Response

{
  "workspaces": 1,
  "users": 12,
  "agents": 8,
  "running": 3
}
FieldTypeDescription
workspacesintegerAlways 1 (scoped to current workspace)
usersintegerNumber of workspace members
agentsintegerTotal agents (excluding soft-deleted)
runningintegerAgents with a currently running agent run
Stats are scoped to the current workspace to prevent cross-workspace data leakage. The workspaces field always returns 1.

GET /api/v1/admin/users

Lists all users in the current workspace with their roles. Required role: OWNER

Response

[
  {
    "id": "user-uuid",
    "email": "alice@example.com",
    "full_name": "Alice Chen",
    "avatar_url": "https://...",
    "created_at": "2025-01-01T00:00:00Z",
    "workspace": {
      "id": "ws-uuid",
      "name": "Engineering",
      "slug": "engineering"
    },
    "role": "ADMIN"
  }
]
FieldTypeDescription
idstringUser ID
emailstringUser email address
full_namestring?Display name (nullable)
avatar_urlstring?Avatar URL (nullable)
created_atstringISO 8601 creation timestamp
workspaceobject?Workspace details (id, name, slug)
rolestring?Role in this workspace (OWNER, ADMIN, MEMBER, VIEWER)

GET /api/v1/admin/workspaces

Lists workspace details with member, agent, and crew counts. Required role: OWNER

Response

[
  {
    "id": "ws-uuid",
    "name": "Engineering",
    "slug": "engineering",
    "created_at": "2025-01-01T00:00:00Z",
    "updated_at": "2025-06-15T12:00:00Z",
    "_count_members": 12,
    "_count_agents": 8,
    "_count_crews": 3
  }
]
This endpoint is scoped to the current workspace only. It does not list other workspaces in the system.

GET /api/v1/admin/memory/stats

Returns aggregate statistics for the memory subsystem within the current workspace — totals, per-tier rollups, and per-agent rollups derived from memory_versions. Required role: ADMIN or OWNER (manage permission)

Response

{
  "workspace_id": "ws-uuid",
  "totals": {
    "versions": 1284,
    "bytes": 4823104,
    "blobs": 612,
    "oldest_at": "2026-03-02T07:14:22Z",
    "newest_at": "2026-05-18T08:30:00Z"
  },
  "by_tier": [
    { "tier": "agent", "versions": 904, "bytes": 3120388 },
    { "tier": "crew", "versions": 220, "bytes": 1102716 },
    { "tier": "workspace", "versions": 160, "bytes": 600000 }
  ],
  "by_agent": [
    { "agent_slug": "", "versions": 380, "bytes": 1702716, "newest_at": "2026-05-18T08:12:11Z" },
    { "agent_slug": "martin", "versions": 412, "bytes": 1880388, "newest_at": "2026-05-18T08:30:00Z" }
  ]
}
FieldTypeDescription
workspace_idstringCurrent workspace ID
totals.versionsintegerTotal memory_versions rows
totals.bytesintegerSum of bytes across all rows
totals.blobsintegerDistinct sha256s; content-identical re-writes share one blob
totals.oldest_atstringRFC3339 of the oldest row; "" when no rows
totals.newest_atstringRFC3339 of the newest row; "" when no rows
by_tier[].tierstringagent, crew, workspace, pins, or learned
by_tier[].versionsintegerRow count for the tier
by_tier[].bytesintegerSum of bytes for the tier
by_agent[].agent_slugstringSlug extracted from canonical agent:<slug>/... prefix; "" for crew/workspace-tier rows
by_agent[].versionsintegerRow count for the agent
by_agent[].bytesintegerSum of bytes for the agent
by_agent[].newest_atstringRFC3339 of the agent’s newest row

Example

curl -H "Authorization: Bearer $TOKEN" \
  https://crewship.example.com/api/v1/admin/memory/stats
StatusCondition
400Missing workspace context
403MEMBER role (manage permission required)
500Underlying SQLite query failure
Tiers with zero rows are omitted from by_tier. Tiers with rows the operator has never written to are NOT included as zero entries.

GET /api/v1/admin/memory/versions

Row-level drill-down into memory_versions. Pairs with the stats endpoint above: stats answers “how much memory does this workspace have?”, versions answers “which rows specifically?”. Results are ordered newest-first by written_at DESC, id DESC and paginated via an opaque keyset cursor. Required role: ADMIN or OWNER (manage permission)

Query Parameters

All parameters are optional and AND-composed.
ParameterTypeDefaultDescription
tierstringExact tier filter: agent, crew, workspace, pins, or learned
agent_slugstringSlug from canonical agent:<slug>/... prefix; literal % / _ in the slug match themselves
path_prefixstringMatch rows whose canonical path starts with this string; literal % / _ match themselves
sincestringRFC3339 lower bound on written_at (inclusive)
untilstringRFC3339 upper bound on written_at (exclusive)
limitinteger50Page size; hard cap at 500
cursorstringOpaque next_cursor from a prior response

Response

{
  "workspace_id": "ws-uuid",
  "rows": [
    {
      "id": "mv_01HZ...",
      "path": "agent:martin/AGENT.md",
      "tier": "agent",
      "sha256": "abc...",
      "bytes": 1234,
      "written_at": "2026-05-18T08:30:00Z",
      "written_by": "audit-watcher",
      "parent_sha": "def..."
    }
  ],
  "next_cursor": "djE6MjAyNi0wNS0xOFQwODozMDowMC4wMDBafG12XzAxSFo...",
  "limit": 50,
  "filters_applied": {
    "tier": "agent",
    "agent_slug": "martin"
  }
}
FieldTypeDescription
workspace_idstringCurrent workspace ID
rows[].idstringmemory_versions.id
rows[].pathstringCanonical memory path
rows[].tierstringTier (agent / crew / workspace / pins / learned)
rows[].sha256stringContent sha256 of the blob
rows[].bytesintegerByte length of the blob
rows[].written_atstringRFC3339 timestamp
rows[].written_bystringWriter identifier; "" when NULL
rows[].parent_shastring?Previous version’s sha256; omitted when NULL
next_cursorstring?Opaque cursor for the next page; null on last page
limitintegerThe resolved page size
filters_appliedobjectEcho of the normalised filters that produced this response
The cursor is a base64url-encoded v1:<rfc3339nano>|<id> tuple pinning (written_at, id). Offset pagination would duplicate or skip rows because the audit watcher writes continuously; keyset pagination pins the boundary so concurrent inserts above the cursor land on the next refresh naturally.

Example

curl -H "Authorization: Bearer $TOKEN" \
  "https://crewship.example.com/api/v1/admin/memory/versions?tier=agent&agent_slug=martin&limit=20"
StatusCondition
400Unknown tier, malformed since / until (not RFC3339), non-positive limit, or unparseable cursor
403MEMBER role
500Underlying SQLite query failure

GET /api/v1/admin/memory/versions/{id}/content

Returns the raw blob bytes for a single memory_versions row. Used by the dashboard’s row-detail view and by compliance auditors who need the literal content (not just the metadata) — for example, to confirm a PII scrubber fired on the offending payload. Required role: ADMIN or OWNER (manage permission)

Response

The body is the raw blob bytes (NOT JSON-wrapped). Content-Type is text/markdown; charset=utf-8 for paths ending in .md, otherwise application/octet-stream so the client cannot auto-render untrusted bytes as HTML. Audit metadata travels alongside the body via response headers:
HeaderDescription
X-Memory-Sha256sha256 from the memory_versions row
X-Memory-BytesLength from the row (matches the row’s recorded size)
X-Memory-TierTier (agent / crew / workspace / pins / learned)
X-Memory-PathCanonical memory path
X-Memory-Written-AtRFC3339 timestamp
X-Memory-Written-ByWriter identifier (omitted when NULL)
Cache-Controlprivate, max-age=31536000, immutable — blobs are content-addressed

Example

curl -H "Authorization: Bearer $TOKEN" \
  -D - \
  https://crewship.example.com/api/v1/admin/memory/versions/mv_01HZ.../content
StatusCondition
400Missing id path segment
401Missing workspace context
403MEMBER role
404Unknown id OR cross-workspace probe (no existence leak)
410Row exists but the blob file is missing on disk (retention sweep, restore-from-backup race)
413Blob exceeds the 10 MB cap (DB-claimed size OR on-disk size)
500sha mismatch (blob tampered after recording) OR payload_ref resolves outside the configured blob root
503Memory versioning is not configured (lite-mode deployment without blob root)
The handler refuses to follow symlinks under payload_ref. The on-disk layout is fixed at blobRoot/<sha[:2]>/<sha>; filepath.EvalSymlinks is used to verify the resolved path stays inside the blob root, defending against path-traversal vectors in a corrupted or malicious payload_ref.

GET /api/v1/admin/memory/config

Returns the per-workspace memory configuration. Drives the retention sweep (versions_retention_days) and is the operator’s read surface for inspecting drift between “what’s stored on the row” and “what’s effective”. Required role: ADMIN or OWNER (manage permission)

Response

{
  "workspace_id": "ws-uuid",
  "versions_retention_days": 30,
  "is_default": true,
  "raw_config": null
}
FieldTypeDescription
workspace_idstringCurrent workspace ID
versions_retention_daysintegerResolved retention window; falls back to the built-in default (30) when no row or no key is set
is_defaultbooleantrue when no row exists, the key is missing, or the value fell back to the default
raw_configstring?Literal JSON stored on the workspaces.memory_config column; null when empty

Example

curl -H "Authorization: Bearer $TOKEN" \
  https://crewship.example.com/api/v1/admin/memory/config
StatusCondition
400Missing workspace context
403MEMBER role
500Stored JSON is malformed (PATCH still works — see below)

PATCH /api/v1/admin/memory/config

Partial-merge update of the per-workspace memory configuration. Merges the request body’s keys into the existing JSON document; unspecified keys are preserved. Required role: ADMIN or OWNER (manage permission)

Request Body

{
  "versions_retention_days": 7
}
FieldTypeDescription
versions_retention_daysintegerPositive integer in [1, 3650]. Zero, negative, fractional, or non-numeric values produce a 400 naming the offending field
Unknown top-level keys are passed through to the stored document for forward compatibility (e.g. future fields like compaction_hour_override).

Response

Returns the post-merge config in the same shape as the GET response above. A PATCH that produces no diff (e.g. resetting to the same value) returns 200 with the current shape and emits NO journal entry — the audit trail tracks actual change, not request count.

Example

curl -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"versions_retention_days": 7}' \
  https://crewship.example.com/api/v1/admin/memory/config
StatusCondition
400Missing workspace context, malformed JSON, trailing garbage after the JSON value, empty body, or versions_retention_days outside [1, 3650] / not a positive integer
403MEMBER role
413Request body exceeds 16 KB
500UPDATE / commit failure
The read-merge-write runs inside a SQLite BEGIN IMMEDIATE (serializable) transaction so concurrent PATCHes touching different keys serialise rather than last-write-wins. Each real diff emits a memory.config_updated journal entry (Notice severity, ActorUser) with payload {workspace_id, changes: {field: {from, to}}}. If the stored JSON is corrupt, PATCH still succeeds (treats the existing document as empty) so operators can fix the row without resorting to manual SQL.

GET /api/v1/admin/keeper/requests

Returns the Keeper access request audit log — every credential access and command execution request evaluated by the Keeper. Required role: ADMIN or OWNER (manage permission)

Query Parameters

ParameterTypeDefaultDescription
limitinteger50Max entries to return (1-200)
offsetinteger0Offset for pagination

Response

[
  {
    "id": "request-uuid",
    "agent_id": "agent-uuid",
    "agent_name": "Viktor",
    "crew_id": "crew-uuid",
    "credential_id": "cred-uuid",
    "credential_name": "Anthropic Production",
    "intent": "Need to call the Claude API to analyze code",
    "request_type": "credential",
    "command": null,
    "decision": "ALLOW",
    "reason": "Low-risk API call with clear intent",
    "risk_score": 2,
    "exit_code": null,
    "ollama_prompt": "...",
    "ollama_raw_response": "...",
    "created_at": "2025-01-15T10:30:00Z",
    "decided_at": "2025-01-15T10:30:01Z"
  }
]
FieldTypeDescription
idstringRequest ID
agent_idstringRequesting agent ID
agent_namestringAgent display name
crew_idstringCrew the agent belongs to
credential_idstringCredential being accessed
credential_namestringCredential display name
intentstringAgent’s stated intent for access
request_typestringcredential or execute
commandstring?Shell command (for execute requests)
decisionstring?ALLOW, DENY, or ESCALATE
reasonstring?LLM-generated explanation
risk_scoreinteger?Risk assessment (1-10 scale)
exit_codeinteger?Command exit code (for execute requests)
ollama_promptstring?Full prompt sent to the Keeper LLM
ollama_raw_responsestring?Raw LLM response text
created_atstringISO 8601 request timestamp
decided_atstring?ISO 8601 decision timestamp

System

Small cross-cutting endpoints that don’t belong to any single domain handler. They surface install state, telemetry consent, the running binary’s version, and dashboard time-series metrics. Three are intentionally unauthenticated because the login page itself needs to read them before any session exists.

GET /api/v1/system/setup-status

First-run gate. Returns whether the install needs to be bootstrapped (empty users table) and whether public signup is enabled. The login page calls this on every page paint — when needs_bootstrap is true, the browser routes to /bootstrap instead of /login. Auth: none — the answer is what tells the browser which page to render.

Response

{
  "needs_bootstrap": false,
  "allow_signup": false
}
FieldTypeDescription
needs_bootstrapbooleantrue when COUNT(*) FROM users = 0. DB errors fail closed as false — a transient blip won’t ship the user into the bootstrap flow on a healthy install.
allow_signupbooleanMirrors the CREWSHIP_ALLOW_SIGNUP server flag. When false, the login page hides the “Sign up” link.

GET /api/v1/system/telemetry

Read-only consent gate for the frontend’s Sentry client. The Next.js sentry.client.config.ts fetches this before calling Sentry.init and bails out if enabled=false. Consent is flipped via the CLI (crewship telemetry on/off), never over HTTP — making this endpoint mutating would create a CSRF vector that flips the bit on every cross-site navigation. Auth: none — the login page must boot crash reporting before any session exists.

Response

{
  "enabled": true,
  "install_id": "a3f9c2e1b8d74f5a"
}
FieldTypeDescription
enabledbooleantrue only when the operator has opted in, a DSN is wired, AND the backend Sentry init succeeded.
install_idstringAnonymous identifier the backend already ships as Sentry’s ServerName. Exposed so frontend events group with backend events for the same install.
Errors fall back to {enabled: false, install_id: ""} rather than 5xx — a transient DB blip defaults to the privacy-preserving outcome.

GET /api/v1/system/version

Reports the running binary’s version and (cache-permitting) the latest release from GitHub. The web UI uses this to render an “update available” banner. Auth: required (any authenticated user, no workspace role needed).

Response

{
  "current": "0.4.1",
  "latest": "0.4.2",
  "newer": true,
  "url": "https://github.com/crewship-ai/crewship/releases/tag/v0.4.2"
}
FieldTypeDescription
currentstringThe running binary’s version (SetVersion-injected from cmd_start).
lateststring?Latest release tag from GitHub. null on a cold cache + GitHub timeout.
newerbooleantrue when latest > current by semver.
urlstring?Release page URL. null paired with null latest.
The handler imposes a 4 s upper bound on top of the update package’s 5 s internal HTTP timeout — a cold cache + slow network still returns “no info” rather than blocking the UI render.

GET /api/v1/metrics/timeseries

Bucketed time-series metrics for the dashboard charts. Returns zero-filled bucket sequences so the client never has to patch visual gaps. Reads workspace_id from the request context, never from a query param. Auth: required + workspace context.

Query parameters

ParamTypeDefaultDescription
metricstringRequired. One of issues_closed, cost_usd, runs_count, active_missions.
windowstring"24h"One of 24h, 7d, 30d.
bucketstring"1h"One of 15m, 1h, 1d. Combinations producing >200 buckets are rejected.
group_bystring"none"One of none, crew, model, status. Some group-by values are metric-specific (see error matrix).

Response

{
  "metric": "issues_closed",
  "window": "7d",
  "bucket": "1d",
  "group_by": "none",
  "buckets": [
    { "ts": "2026-05-14T00:00:00Z", "series": { "total": 4 } },
    { "ts": "2026-05-15T00:00:00Z", "series": { "total": 0 } },
    { "ts": "2026-05-16T00:00:00Z", "series": { "total": 7 } }
  ],
  "series_labels": { "total": "Total" }
}
FieldTypeDescription
metric / window / bucket / group_bystringEcho of the resolved query parameters.
buckets[].tsstringBucket-start in UTC, aligned to wall-clock boundaries (15m:00/:15/:30/:45, 1h → top of hour, 1d → UTC midnight).
buckets[].seriesobjectMap of series key to numeric value. Always floats on the wire so cost and counts share the JSON shape.
series_labelsobjectMap of series key to display label (e.g. crew name, model name, status). For group_by=none always contains {"total": "Total"}.
StatusCondition
400Unknown metric / window / bucket / group_by; bucket larger than window; combination produces >200 buckets; group_by=model for a metric other than cost_usd; group_by=crew for a metric other than issues_closed/runs_count; group_by=status for a metric other than issues_closed/active_missions.
401Workspace context missing from the request.
500SQLite fill query failed.

License System

Roadmap (v0.2). The license/edition system below is on the v0.2 roadmap. v0.1 ships as fully open-source Apache-2.0 with no edition gating.
Crewship will use a three-tier licensing model that controls workspace limits and feature availability.

Editions

EditionDescriptionLimits
CommunityFree, default edition15 crews, 10 agents/crew, 5 members
TeamPaid team tierHigher limits, additional features
EnterpriseFull enterprise tierUnlimited, all features enabled

License Claims

Each license contains signed claims:
ClaimTypeDescription
license_idstringUnique license identifier
licensee_namestringLicensed user name
licensee_orgstringLicensed organization
editionstringcommunity, team, or enterprise
max_crewsintegerMaximum number of crews
max_agents_per_crewintegerMaximum agents per crew
max_membersintegerMaximum workspace members
featuresstring[]Enabled feature flags
issued_atintegerUnix timestamp of issuance
expires_atintegerUnix timestamp of expiration

License Verification

Licenses are verified using Ed25519 digital signatures:
1

Signed format

A license file contains a JSON object with payload (the claims as a JSON string) and signature (base64-encoded Ed25519 signature).
2

Public key embedding

The Ed25519 public key is embedded into the binary at build time via ldflags. This prevents license tampering by tying verification to the specific build.
3

Signature verification

On startup, Crewship decodes the public key and signature from base64, then verifies the payload using ed25519.Verify().
4

Expiration check

If the license has an expires_at timestamp and it is in the past, the license is rejected and community defaults apply.
5

Fallback

If no license file exists, verification fails, or the license is expired, Crewship runs with community edition defaults.
The public key variable is set at build time. Without a valid public key embedded in the binary, license loading will fail with “no public key embedded in binary” and community defaults will apply.

What’s Next

RBAC

Role-based access control and permission levels.

Keeper Guide

Configure the AI-powered security gatekeeper.