REST API
The SessionFS API server exposes a REST API for session management, auditing, sync, and settings.
Base URL
Section titled “Base URL”- Cloud:
https://api.sessionfs.dev - Self-hosted: Your deployment URL
Authentication
Section titled “Authentication”All endpoints (except signup) require a Bearer token:
Authorization: Bearer sk_sfs_...Two key kinds:
- User keys — created via
POST /api/v1/auth/keys. Tied to a single user, inherit all of that user’s permissions. Legacy keys are back-filled toscopes=["*"]. - Service keys (v0.10.10+) — created via
POST /api/v1/orgs/{org_id}/service-keys. Org-scoped, expirable, and restricted to an enumeratedscopeslist (e.g.["handoffs:write", "agent_runs:write"]). Deny-by-default: a service key only reaches a route handler that explicitly opted in viarequire_scope(...). The recommended credential for cloud agents (Bedrock, Vertex), CI runners (GitHub Actions, GitLab MR), and integration partners.
A project you can’t read returns the same 404 as a project that doesn’t exist (v0.29.11; it was 403 before). Editor-only refusals for readers stay 403.
Structured 401/403 error codes: api_key_revoked, api_key_expired, service_key_not_allowed, insufficient_scope (with required + current arrays), cross_org_denied, service_key_project_required, service_key_project_not_registered, service_key_project_ambiguous.
Stability, deprecation headers, the X-SessionFS-Ignored-Fields header and the committed OpenAPI snapshot are covered in API Contract and Versioning.
Idempotent requests
Section titled “Idempotent requests”A network retry of a POST can run it twice: two tickets, two release records, two eval runs. Send an Idempotency-Key header and the server runs the request at most once for that key (v0.30.0):
POST /api/v1/projects/proj_123/ticketsAuthorization: Bearer sk_sfs_...Idempotency-Key: 6f1c2f0e-4a51-4f6e-9a0e-2b8b3f7d9c11Content-Type: application/json- The key is 1 to 255 printable ASCII characters; a random UUID per logical request is the usual choice. Anything else returns
400 idempotency_key_invalid. - Keys are scoped to your credential, the HTTP method and the route, so the same key sent with a different API key, or to a different route, never collides.
- Retry with the same key and the same body. The first completed response is stored and replayed: same status code, same body, the same
Content-Type,ETagandLocationheaders, plusIdempotent-Replayed: true. JSON bodies are compared by value, so key order and whitespace don’t matter; the path and query string must match. - Stored responses are kept for 24 hours. After that the key is forgotten and a reuse runs the request again.
- Without the header, nothing changes.
The sfs CLI and the MCP server send a fresh key on every POST.
| Status | Code | Meaning |
|---|---|---|
409 |
idempotency_in_progress |
A request with this key is still running. Retry later with the same key. |
422 |
idempotency_key_reused |
The key was already used for a different request (another path, query or body). Use a new key. |
409 |
idempotency_response_unavailable |
The request already ran but its response wasn’t stored (larger than 1 MiB, or it held a credential), so it can’t be replayed. Check the result before sending it again with a new key. |
503 |
idempotency_unavailable |
The key couldn’t be recorded. Nothing ran; retry. |
What is stored and what isn’t:
2xxresponses and most4xxresponses are stored and replayed.- A
5xx, a failed request, or a passing refusal (401,408,409,423,425,429) is not stored: the key is released, so a retry with the same key runs the request again. - A request still running when its lease runs out (5 minutes; 30 minutes for routes that call a model or another slow service, such as extraction, compile and Jira publish) can be taken over by a retry with the same key.
These requests ignore the header and run exactly as they would without it:
- requests with no
Authorization: Bearercredential, and request bodies over 4 MiB; POST /api/v1/helper/turns(it streams Server-Sent Events);- webhook ingest (
/api/v1/webhooks/*and/webhooks/*), which already de-duplicates by each provider’s delivery id; - session sync uploads (
POST /api/v1/sessions,POST /api/v1/sessions/{session_id}/uploads,POST /api/v1/uploads/{upload_id}/complete), which have their own resume and completion idempotency; - routes that are unauthenticated or use a credential other than an API key: signup, SSO start and exchange, license activation, Helm license validation, telemetry, public share views and trusted-publishing token exchange and publish;
- routes whose response shows a secret once, which is never stored: user and service API key create and rotate, admin key minting, resident key rotation, delivery and Jira webhook secrets, session and loop share links, license creation and the GitHub OAuth start and choose steps.
Endpoints
Section titled “Endpoints”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/signup |
None | Create account |
| GET | /api/v1/auth/me |
Bearer | Current user profile. orgs lists every organization you belong to (org_id, name, slug, role, joined_at, is_default); org_id, org_name and org_role describe your default org. See Your default organization |
| POST | /api/v1/auth/keys |
Bearer | Create user API key |
| GET | /api/v1/auth/keys |
Bearer | List user API keys |
| DELETE | /api/v1/auth/keys/{key_id} |
Bearer | Delete a user API key |
| GET | /api/v1/auth/verify/{token} |
None | Verify email address |
| GET | /api/v1/me/ticket-notifications |
Bearer | Your ticket notifications (unread by default); also GET .../unread-count and POST .../read (v0.24.0) |
| GET | /api/v1/me/ticket-updates |
Bearer | What changed on the tickets you follow since your cursor (per persona; service keys get their own cursors); acknowledge with POST /api/v1/me/ticket-updates/ack (v0.24.0) |
| GET / PUT | /api/v1/me/notification-settings |
Bearer | Ticket email digest preference: off, your_turn (default) or all (v0.24.0) |
Service Keys (v0.10.10+)
Section titled “Service Keys (v0.10.10+)”Org-scoped, scope-restricted API keys for cloud agents, CI runners, and integration partners. Org admin role + Team+ tier required for mutations. Raw key returned exactly once on create + rotate; the list endpoint only returns key_prefix.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/orgs/{org_id}/service-keys |
Bearer (org admin) | Create a service key (body: name, scopes[], optional expires_at, project_ids[]) |
| GET | /api/v1/orgs/{org_id}/service-keys |
Bearer (org member) | List service keys for the org |
| DELETE | /api/v1/orgs/{org_id}/service-keys/{id} |
Bearer (org admin) | Revoke a service key (body: optional revoke_reason) |
| POST | /api/v1/orgs/{org_id}/service-keys/{id}/rotate |
Bearer (org admin) | Issue a new raw secret for an existing key |
Available scopes (the * wildcard is reserved for legacy user/admin keys; service keys must enumerate explicitly):
| Scope | Status | Routes opted in |
|---|---|---|
handoffs:read, handoffs:write |
✅ live (v0.10.10) | Handoff create/claim/revoke/decline + comments + events |
agent_runs:write |
✅ live (v0.10.10) | POST /agent-runs, POST /agent-runs/{id}/complete, POST /agent-runs/{id}/usage |
tickets:read, tickets:write |
✅ live (v0.10.18) | Ticket list/get/comments/review-state/readiness + start/complete/comment |
meetings:read, meetings:write |
✅ live (v0.29.0) | Stored meeting transcripts: upload/list/get/delete + participant resolution (see Meetings) |
knowledge:read, knowledge:write |
✅ live (v0.10.19; project context + wiki since v0.29.1) | Knowledge entries: GET /entries, GET /entries/{id}, POST /entries/add, PUT /entries/{id} (dismiss/update/refresh/promote/supersede). Project context (knowledge:read): GET /projects/{id}, GET /projects/{id}/context/sections/{slug}. Wiki (knowledge:read): list/get pages, page history and backlinks; (knowledge:write): PUT /projects/{id}/pages/{slug} — writes record actor_type="service_key" + service_key_id/service_key_name |
personas:read, personas:write |
✅ live (v0.10.20) | Persona list/get + create/update/delete |
agent_runs:read |
✅ live (v0.10.21) | GET /agent-runs, GET /agent-runs/{run_id} |
loops:read, loops:write |
✅ live (v0.29.2) | Review loops: GET /projects/{id}/loops, GET /projects/{id}/loops/{loop_id}, POST /projects/{id}/loops, PATCH /projects/{id}/loops/{loop_id}, plus the loop report, insights, story and checklist routes (see Review Loop API). User keys also need editor standing; cost_budget_usd is org-admin only, and service keys never see or set dollars |
agent_feedback:read, agent_feedback:write |
✅ live (v0.17) | GET /agent-feedback, GET /agent-feedback/summary, POST /agent-feedback |
marketplace:read, marketplace:write |
✅ live (v0.16) | Marketplace search/inspect/download/feed + personal package/registry publishing |
skills:read, skills:request, skills:approve, skills:admin |
✅ live (v0.16) | Approved instruction load, install-request lifecycle, org registries/packages/assignments |
skills_verify:run |
✅ live (platform, tier-coupled) | The works-in harness protocol (claim / projection / result). Only a platform admin may mint or rotate a key with it, and the key works only while the user it runs as and the admin who minted or last rotated it are both still active platform admins |
skills_registry:poll |
✅ live (platform, tier-coupled) | The registry poller’s scheduler credential (POST /api/v1/admin/skills/registries/poll). Only a platform admin may mint or rotate a key with it, and the key works only while the user it runs as and the admin who minted or last rotated it are both still active platform admins |
platform_sweeps:run |
✅ live (platform, tier-coupled) | The scheduled sweeps’ credential: catalog regenerate, verify sweep, freshness sweep, rescan, publisher recheck, the GitHub webhook-delivery sweep, and (v0.23.0) the KB compile, org digest and helper retention sweeps. Only a platform admin may mint or rotate a key with it, and the key works only while the user it runs as and the admin who minted or last rotated it are both still active platform admins |
admin_metrics:read |
✅ live (platform, tier-coupled, v0.23.0) | GET /api/v1/admin/metrics (Prometheus). Only a platform admin may mint or rotate a key with it, and the key works only while the user it runs as and the admin who minted or last rotated it are both still active platform admins |
sessions:read, rules:read, rules:write, retrieval_audit:read, admin:* |
Reserved | Not yet opted in on any route |
Tier-coupled scopes. The four platform scopes above are tier-coupled: they are mint/rotate-only by a platform admin and the key keeps working only while both the user it runs as and the admin who minted or last rotated it are still active platform admins. Demoting or deactivating either one stops the key with a 403 that gives a coarse reason (minter_not_admin, owner_not_admin or minter_missing) and the fix (restore the minter, or mint a new key as a user who stays a platform admin); it never names another user or their plan, since the key’s holder may not be its minter. Minting and rotating a tier-coupled key returns a warnings array with a minter_tier_coupling entry so the operator knows before they build a job around it, and service-keys list exposes coupling_ok (true/false/null) plus coupling_reason so a silently-broken key is visible. Platform admins see which account lost standing (minter no longer admin / owner no longer admin); other org admins see platform admin standing lapsed. Making someone a platform admin just long enough to mint a key doesn’t work: the key stops as soon as they’re demoted (v0.28.0 makes this visible; the rule itself is unchanged).
Sessions
Section titled “Sessions”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/sessions |
Bearer | List sessions (paginated). Query params: page, page_size, source_tool, model (repeatable, exact match, OR’d, deduplicated; at most 50 values of at most 200 characters each, else 422; model=unknown matches the unknown bucket: a NULL, empty, literal unknown or <synthetic> model id), tag, sort (updated_at default, or model; other values are a 422), order (asc/desc, default desc). sort=model puts unknown models last in both directions |
| GET | /api/v1/sessions/models |
Bearer | Distinct model ids among your visible sessions with counts (model_id: null is the aggregated “unknown” bucket: NULL, empty, literal unknown and <synthetic> ids), sorted by count desc; honors source_tool and deleted |
| GET | /api/v1/sessions/{id} |
Bearer | Get session detail |
| PUT | /api/v1/sessions/{id}/sync |
Bearer | Push session (upload archive). If-Match makes it conditional (409 etag_mismatch when stale, checked before the archive is processed); every response carries a Server-Timing header with the push’s phase breakdown |
| GET | /api/v1/sessions/{id}/sync |
Bearer | Pull session (download archive) |
| GET | /api/v1/sessions/{id}/messages |
Bearer | Get paginated messages. Each page lists the messages.jsonl line number of every message (message_indexes); ?around=<n> returns the page holding line n with a target saying whether it was found, hidden by the view, or missing |
| GET | /api/v1/sessions/{id}/summary |
Bearer | Get session summary |
| POST | /api/v1/sessions/{id}/summary |
Bearer | Generate session summary |
| POST | /api/v1/sessions |
Bearer | Upload/create a session |
| DELETE | /api/v1/sessions/{id} |
Bearer | Soft-delete a session |
| PUT | /api/v1/sessions/{id}/alias |
Bearer | Set session alias |
| DELETE | /api/v1/sessions/{id}/alias |
Bearer | Clear session alias |
| POST | /api/v1/sessions/{id}/share |
Bearer | Create share link |
| DELETE | /api/v1/sessions/{id}/share/{link_id} |
Bearer | Revoke share link |
| GET | /api/v1/sessions/share/{token} |
None | Access shared session (public) |
| POST | /api/v1/sessions/share/{token} |
None | Access password-protected share link |
| GET | /api/v1/sessions/search |
Bearer | Full-text search |
| GET | /api/v1/sessions/{id}/changes |
Bearer | Files the session created, edited or deleted, with line counts and per-file diffs (v0.23.0) |
| POST | /api/v1/sessions/{id}/uploads |
Bearer | Start a resumable upload for a session over 8 MiB; then PUT /api/v1/uploads/{upload_id}/parts/{n}, GET /api/v1/uploads/{upload_id}, POST /api/v1/uploads/{upload_id}/complete (v0.23.0). The CLI and daemon use this automatically |
Session Helper (Enterprise)
Section titled “Session Helper (Enterprise)”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/helper/turns |
Bearer (user key) | Ask a question; Server-Sent Events by default, one JSON document with stream: false. scope takes at most one of project_id, group_id (a project group; v0.28.0) or session_ids. See Session Helper |
| GET | /api/v1/helper/conversations |
Bearer (user key) | List your helper threads |
| GET | /api/v1/helper/conversations/{id} |
Bearer (user key) | One thread with its messages |
| DELETE | /api/v1/helper/conversations/{id} |
Bearer (user key) | Delete a thread |
| GET | /api/v1/helper/status |
Bearer (user key) | Whether the helper is enabled, which key it would use, and its effective settings |
| GET | /api/v1/helper/actions/{id} |
Bearer (user key) | A change the helper proposed (move sessions to Trash, turn Auto Narrative on or off), with its targets and state (v0.25.0) |
| POST | /api/v1/helper/actions/{id}/confirm |
Bearer (user key) | Apply a proposed change; access is re-checked for every target and per-target results are returned. Proposals expire after 15 minutes (v0.25.0) |
| POST | /api/v1/helper/actions/{id}/cancel |
Bearer (user key) | Drop a proposed change (v0.25.0) |
| GET / PUT | /api/v1/settings/helper |
Bearer (user key) | Your helper settings, custom instructions and this month’s usage. PUT merges (omitted keeps, null clears) |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/sessions/{id}/audit |
Bearer | Run LLM Judge audit |
| GET | /api/v1/sessions/{id}/audit |
Bearer | Get latest audit report |
| GET | /api/v1/sessions/{id}/audits |
Bearer | Audit history |
| GET | /api/v1/sessions/{id}/audit/status |
Bearer | Check background audit status |
Handoffs
Section titled “Handoffs”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/handoffs |
Bearer | Create handoff |
| GET | /api/v1/handoffs/{id} |
Bearer | Get handoff details |
| POST | /api/v1/handoffs/{id}/claim |
Bearer | Claim handoff (copies session) |
| GET | /api/v1/handoffs/inbox |
Bearer | Handoffs received |
| GET | /api/v1/handoffs/sent |
Bearer | Handoffs sent |
| GET | /api/v1/handoffs/{id}/summary |
None | Get handoff session summary |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/sync/settings |
Bearer | Get autosync settings (mode, debounce_seconds, max_member_bytes — tier-aware per-file cap; CLI v0.10.27+ reads this to avoid a hardcoded fallback) |
| PUT | /api/v1/sync/settings |
Bearer | Update autosync mode (requires autosync feature if not “off”); response echoes the same shape as GET including max_member_bytes |
| GET | /api/v1/sync/watchlist |
Bearer | Get watched sessions |
| POST | /api/v1/sync/watch/{id} |
Bearer | Add session to watchlist |
| DELETE | /api/v1/sync/watch/{id} |
Bearer | Remove session from watchlist |
| PUT | /api/v1/sync/watch/{id}/{status} |
Bearer | Update watch status (pending/queued/synced/failed) |
| GET | /api/v1/sync/status |
Bearer | Sync status overview (counts, storage usage) |
Settings
Section titled “Settings”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/settings/judge |
Bearer | Get judge settings |
| PUT | /api/v1/settings/judge |
Bearer | Save judge settings. A new custom base_url (or a new key with one) is tested first: 422 {code, message} for endpoint_not_api / invalid_key / endpoint_blocked; transient failures save and return warnings |
| DELETE | /api/v1/settings/judge |
Bearer | Clear judge settings |
| GET | /api/v1/settings/judge/models?base_url= |
Bearer | Discover models from custom endpoint using the saved judge key (an api_key query parameter is refused) |
| POST | /api/v1/settings/judge/models |
Bearer | Discover models; body {base_url, api_key?} |
| POST | /api/v1/settings/judge/test |
Bearer | Test connection: one minimal chat request. Body {provider?, model?, base_url?, api_key?}; without api_key the saved key is used only for the provider/Base URL it was saved with (400 key_required otherwise). Returns {ok, code, message, provider, model, endpoint_host, latency_ms}. 10 per minute (429 test_rate_limited) |
| GET | /api/v1/settings/audit-trigger |
Bearer | Get auto-audit trigger |
| PUT | /api/v1/settings/audit-trigger |
Bearer | Set auto-audit trigger |
| GET | /api/v1/settings/github |
Bearer | GitHub installation settings |
| PUT | /api/v1/settings/github |
Bearer | Update GitHub settings |
| GET | /api/v1/settings/gitlab |
Bearer | GitLab integration settings |
| PUT | /api/v1/settings/gitlab |
Bearer | Update GitLab settings |
| DELETE | /api/v1/settings/gitlab |
Bearer | Remove GitLab integration |
Projects
Section titled “Projects”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/projects/ |
Bearer | List projects the user has access to |
| POST | /api/v1/projects/ |
Bearer | Create project context (requires project_context feature) |
| GET | /api/v1/projects/{remote} |
Bearer | Get project context by git remote |
| PUT | /api/v1/projects/{remote}/context |
Bearer | Update context document |
| DELETE | /api/v1/projects/{project_id} |
Bearer | Delete project (owner or admin only) |
| GET | /api/v1/projects/{id}/compile-status |
Bearer | Last compile, claims waiting, whether a compile is recommended (v0.23.0) |
| GET | /api/v1/projects/{id}/personas/{name}/rollup |
Bearer | Persona activity: recent sessions, open tickets, latest knowledge entry and agent run (v0.23.0) |
| POST | /api/v1/projects/{id}/agent-runs/{run_id}/replay |
Bearer | Queue a copy of a run with an edited prompt, ticket, persona, context references, fail-on or trigger ref (v0.23.0) |
| POST | /api/v1/projects/{id}/agent-runs/{run_id}/usage |
Bearer (agent_runs:write) |
Add token/cost usage to a queued or running run (additive; 409 after completion) (v0.29.1) |
| GET | /api/v1/projects/{id}/agent-runs/{run_id}/compare/{other_id} |
Bearer | Findings added, removed or unchanged between two runs (v0.23.0) |
| GET | /api/v1/projects/{id}/spend |
Bearer (agent_runs:read or tickets:read) |
Agent-run spend for the project. Query: group_by (persona, model, ticket, role or day), from / to (ISO datetimes; default the last 30 days), work_queue_id. Token totals always; cost_usd only for org owners/admins or the owner of a personal project, never for service keys. Only reported and estimated runs add dollars; plan and unknown runs add to tokens_on_plans (v0.29.1) |
| GET | /api/v1/projects/{id}/tickets/{tid}/spend |
Bearer (agent_runs:read or tickets:read) |
Spend for one ticket: totals, per persona, per model and the number of review rounds, with the same dollar rules (v0.29.1) |
Knowledge Base
Section titled “Knowledge Base”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/projects/{id}/entries |
Bearer | List knowledge entries. Query params: type, pending, search, limit |
| POST | /api/v1/projects/{id}/entries/add |
Bearer | Create a knowledge entry (types: decision, pattern, discovery, convention, bug, dependency) |
| PUT | /api/v1/projects/{id}/entries/{entry_id} |
Bearer | Dismiss or un-dismiss a knowledge entry |
| POST | /api/v1/projects/{id}/compile |
Bearer | Compile pending entries into project context (optional LLM config in body) |
| GET | /api/v1/projects/{id}/compilations |
Bearer | List compilation history. Query param: limit |
| GET | /api/v1/projects/{id}/health |
Bearer | Knowledge health stats (total/pending/compiled/dismissed entries, staleness) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/projects/{id}/pages |
Bearer | List all wiki pages for a project |
| GET | /api/v1/projects/{id}/pages/{slug} |
Bearer | Get a wiki page with backlinks |
| PUT | /api/v1/projects/{id}/pages/{slug} |
Bearer | Create or update a wiki page (requires project_context feature) |
| DELETE | /api/v1/projects/{id}/pages/{slug} |
Bearer | Delete a wiki page (requires project_context feature) |
| POST | /api/v1/projects/{id}/pages/{slug}/regenerate |
Bearer | Regenerate an auto-generated concept page from latest entries |
| GET | /api/v1/projects/{id}/links/{target_type}/{target_id} |
Bearer | Get backlinks for a target |
| PUT | /api/v1/projects/{id}/settings |
Bearer | Update project settings (e.g. auto_narrative toggle) |
Tickets
Section titled “Tickets”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/projects/{id}/tickets |
Bearer (tickets:read) |
List tickets. Query params: status, assigned_to, assigned_user (a user id, or me with a user key; v0.27.0), priority, kind, overdue=true (unfinished tickets past their due date; v0.28.0) |
| GET | /api/v1/projects/{id}/assignable-users |
Bearer (tickets:read, user key) |
Teammates who can read the project and can be assigned a ticket directly, with whether each can edit it (v0.27.0) |
| POST | /api/v1/projects/{id}/tickets |
Bearer (tickets:write) |
Create a ticket (body: title, description, acceptance_criteria, priority, kind, parent_ticket_id, depends_on, and either assigned_to (persona) or assigned_user (teammate email or user id; v0.27.0); optional due_date (YYYY-MM-DD) and estimated_effort (Jira duration such as 2d 4h; v0.28.0)) |
| GET | /api/v1/tickets/{tid} |
Bearer (tickets:read) |
Look a ticket up by id alone (for links): the ticket detail plus its project name, comment count and latest comments. Not found and no access both return 404 (v0.25.0) |
| GET | /api/v1/projects/{id}/tickets/{tid} |
Bearer (tickets:read) |
Get a ticket with dependencies, dependents, and recent comments. Includes created_by and resolved_by (name and persona), due_date and estimated_effort (v0.28.0) |
| GET | /api/v1/projects/{id}/tickets/{tid}/events |
Bearer (tickets:read) |
The ticket’s change history, newest first: each event has action, field, before, after, actor and created_at. Query: limit (1–200, default 50) and before (an event id; pass next_before from the previous page) (v0.28.0) |
| PUT | /api/v1/projects/{id}/tickets/{tid} |
Bearer (tickets:write) |
Partial mutation for title / description / priority / acceptance_criteria / context_refs / file_refs / depends_on / assigned_user (teammate email or user id, null to unassign; v0.27.0) / due_date / estimated_effort (null clears either; v0.28.0). Status transitions remain FSM-only. Authz: creator OR project admin; a project editor may change only the assignee (assigned_to / assigned_user, including null to unassign) and gets 403 for any other field. Optional lease_epoch for optimistic concurrency (409 + structured envelope on stale; last-write-wins when omitted; lease_epoch alone rejected with 400 — it’s a fence, not a mutation). Side effect: every successful update auto-posts ONE author_persona='system' diff comment + writes per-field rows to the ticket_edits audit table (no-op writes don’t pollute the audit). depends_on updates run same-project + cycle + dedup validation before wipe-and-reinsert. (v0.10.28) |
| POST | /api/v1/projects/{id}/tickets/{tid}/start |
Bearer (tickets:write) |
Atomic claim. Increments and returns lease_epoch, creates a server-side retrieval_audit_id, returns compiled persona + ticket context |
| POST | /api/v1/projects/{id}/tickets/{tid}/complete |
Bearer (tickets:write) |
in_progress → review. Pass lease_epoch to reject stale workers with 409 |
| POST | /api/v1/projects/{id}/tickets/{tid}/resolve |
Bearer (tickets:write) |
review → done. Atomic with rowcount-1 guard |
| POST | /api/v1/projects/{id}/tickets/{tid}/close |
Bearer (tickets:write) |
Issue terminator (rejects kind='task' with 400) |
| POST | /api/v1/projects/{id}/tickets/{tid}/approve |
Bearer (tickets:write) |
Move suggested → open so an agent-source ticket can be started |
| POST | /api/v1/projects/{id}/tickets/{tid}/escalate |
Bearer (tickets:write) |
Raise priority and append a non-idempotent audit comment |
| POST | /api/v1/projects/{id}/tickets/{tid}/comments |
Bearer (tickets:write) |
Append a comment to the ticket thread |
| GET | /api/v1/projects/{id}/tickets/{tid}/comments |
Bearer (tickets:read) |
Paginated comment thread (query: since, since_id, limit) |
| GET | /api/v1/projects/{id}/tickets/{tid}/review-state |
Bearer (tickets:read) |
Derived review state — open/closed findings, last verdict, severity counts |
| GET | /api/v1/projects/{id}/tickets/{tid}/readiness?sha=<commit> |
Bearer (tickets:read) |
Merge readiness at a full head commit id: {ready, reasons, verdict}. Ready only when a trusted strict VERIFIED-CLEAN verdict is bound to exactly that commit, came from an identity other than every recorded implementer, and no open finding is harmful (v0.29.1) |
| POST | /api/v1/projects/{id}/tickets/{tid}/jira/preview |
Bearer (tickets:write) |
Draft a Jira story from the ticket (Enterprise, v0.23.0). See Jira Cloud |
| POST | /api/v1/projects/{id}/tickets/{tid}/jira/publish |
Bearer (tickets:write) |
Create or update the linked Jira issue. Overwriting a linked issue that was never published from SessionFS needs overwrite_existing: true (else 409 jira_overwrite_required). Optional sprint_id places the issue in a sprint; fields Jira refused are listed in warnings (v0.28.0) |
| GET | /api/v1/projects/{id}/jira/sprints |
Bearer (tickets:write) |
Active and future sprints of the mapped Jira project, for the publish dialog (v0.28.0) |
| GET | /api/v1/projects/{id}/tickets/{tid}/jira |
Bearer (tickets:read) |
Linked Jira issue and cached status |
| DELETE | /api/v1/projects/{id}/tickets/{tid}/jira |
Bearer (tickets:write, project admin) |
Unlink the Jira issue |
| POST | /api/v1/projects/{id}/tickets/{tid}/jira/link |
Bearer (tickets:write, ticket editor) |
Link the ticket to an existing Jira issue (issue_key); nothing is sent to Jira (v0.27.0) |
| POST | /api/v1/projects/{id}/tickets/{tid}/jira/pull |
Bearer (tickets:write, user key, ticket editor) |
Replace the ticket’s title and description with the linked issue’s (confirm: true); empty Jira fields leave the ticket unchanged (v0.27.0) |
Review Loop (v0.29.2)
Section titled “Review Loop (v0.29.2)”Start and watch a worker review loop, set its budgets and keep the project checklist. Tier: Team+ for loops; the checklist is on every plan.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/projects/{id}/loops |
Bearer (loops:write) |
Start a loop on an existing issue (from_ticket_id, or its alias ticket_id as on workflow start; two different values are a 422) or a new one (title). Files one child task per entry in tickets and creates the reviewer’s work queue. Optional token_budget, cost_budget_usd (org owners and admins only) and max_review_rounds (no default cap) |
| GET | /api/v1/projects/{id}/loops |
Bearer (loops:read) |
The project’s loops with status, children, spend and budget |
| GET | /api/v1/projects/{id}/loops/{loop_id} |
Bearer (loops:read) |
One loop: each child’s status, branch, head commit, review rounds, harm, tokens, budget use and merge readiness |
| PATCH | /api/v1/projects/{id}/loops/{loop_id} |
Bearer (loops:write) |
Raise or clear a budget. Only the fields you send change; an explicit null clears a limit. Dollar limits need org owner or admin standing |
| POST | /api/v1/projects/{id}/loops/{loop_id}/children/{ticket_id}/close |
Bearer (user key only) | Close an item the loop handed back as needs_human. People only: service keys get 403. The ticket keeps its own status |
| GET | /api/v1/projects/{id}/tickets/{tid}/loop-report |
Bearer (loops:read) |
Per review round: findings by harm, known versus new checklist classes, design-stage findings and the tokens (and dollars, for admins) the round cost |
| GET | /api/v1/projects/{id}/loop-insights |
Bearer (loops:read) |
Findings caught, cost per accepted change by month and how many new checklist classes were learned. Query: from, to (window up to 366 days) |
| GET | /api/v1/projects/{id}/tickets/{tid}/story |
Bearer (loops:read) |
The ticket’s ordered event stream: brief, run, round, finding, fix, verdict, merge |
| GET | /api/v1/projects/{id}/loop-checklist |
Bearer | List checklist items. Query: status (proposed, active, retired) |
| GET | /api/v1/projects/{id}/loop-checklist.md |
Bearer | Active items as markdown for a brief |
| POST | /api/v1/projects/{id}/loop-checklist |
Bearer (editor) | Propose an item (title, optional check_text and slug) |
| POST | /api/v1/projects/{id}/loop-checklist/seed |
Bearer (editor) | Offer the generic starter items as proposals |
| PATCH | /api/v1/projects/{id}/loop-checklist/{item_id} |
Bearer (editor) | Accept, retire or edit an item |
| POST | /api/v1/projects/{id}/loop-share-links |
Bearer (loops:write, editor, Team+) |
Mint a revocable, expiring public link to a story ({"kind": "story", "ticket_id": ...}) or a ship report (`{“kind”: “ship_report”, “release_version” |
| GET | /api/v1/projects/{id}/loop-share-links |
Bearer (loops:read, editor) |
List a project’s share links newest first; tokens are never returned |
| DELETE | /api/v1/projects/{id}/loop-share-links/{link_id} |
Bearer (loops:write, editor) |
Revoke a share link (soft — the row is kept for audit) |
| GET | /api/v1/share/loop/{token} |
None | Access a shared loop story (public, unauthenticated). Serves a DLP-redacted story with dollars and every author identity stripped; unknown, revoked, expired and deleted links all return the same 404 |
Dollar fields appear only for org owners and admins, or the owner of a personal project, and are left out of the response for everyone else. Loop share links work the same way: the public story omits dollar fields, redacts secrets and emails, and is served with Cache-Control: no-store and X-Robots-Tag: noindex.
Worker runs, review rounds, merge checks and releases (v0.29.3)
Section titled “Worker runs, review rounds, merge checks and releases (v0.29.3)”Tier: Team+ for every write below (including merge checks, release recording, share-link creation and the App/webhook ingest of pull requests and releases). Reads, share-link list/revoke and the public share view stay available after a downgrade.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/projects/{id}/agent-runs/start-worker |
Bearer (loops:write) |
Take a loop ticket’s lease and open a worker run (ticket_id, optional model, provider, branch, head_sha, tool). Returns the run id, the lease epoch and the remaining budget. loop_needs_human when the item is parked or a budget is used up |
| POST | /api/v1/projects/{id}/agent-runs/{run_id}/finish |
Bearer (loops:write) |
Finish a worker run with head_sha, exit_reason (finished, out_of_turns, crashed) and token usage. Repeating it with the same head_sha never counts usage twice; a different head_sha is 409 |
| POST | /api/v1/projects/{id}/loops/{loop_id}/rounds |
Bearer (loops:write) |
Record a review round (ticket_id, round, stage, reviewed_commit, verdict, findings, reviewer usage). Safe to repeat. Trust comes from the caller’s credential, never the body |
| POST | /api/v1/projects/{id}/tickets/{tid}/merge-check |
Bearer (loops:write, editor) |
Recompute the ticket’s merge readiness at its head commit and post or update the “SessionFS review loop” check on GitHub (or a pull request comment when the app lacks the Checks permission) |
| POST | /api/v1/projects/{id}/pull-requests |
Bearer (loops:write) |
Record a pull request and link it to tickets |
| POST | /api/v1/projects/{id}/releases |
Bearer (loops:write) |
Record a release at a commit |
| POST | /api/v1/projects/{id}/releases/{version}/gates |
Bearer (loops:write) |
Record a release gate (for example ci passed) |
| POST | /api/v1/projects/{id}/releases/{version}/deployments |
Bearer (loops:write) |
Record a deployment of a release to an environment |
| GET | /api/v1/projects/{id}/releases |
Bearer (loops:read) |
The project’s releases with gates and deployments |
| GET | /api/v1/projects/{id}/releases/{version}/ship-report |
Bearer (loops:read) |
Ship Report for a release: loops, review rounds, findings, harmful findings and whether all were fixed before merge, cost per accepted change, checklist classes learned, gates, deploys, pull requests and a markdown snippet for release notes |
| GET | /api/v1/projects/{id}/ship-report |
Bearer (loops:read) |
The same report for a month (month=YYYY-MM) |
| POST | /api/v1/orgs/{org_id}/delivery-webhooks/{provider}/rotate |
Bearer (org owner or admin) | Generate or rotate the GitHub or GitLab webhook secret; returned once |
| GET | /api/v1/orgs/{org_id}/delivery-webhooks/{provider}/status |
Bearer (org owner or admin) | Whether a secret exists, its last four characters and the inbound URL |
| POST | /api/v1/webhooks/github/{org_id}, /api/v1/webhooks/gitlab/{org_id} |
Provider signature or token | Inbound delivery webhooks. Unknown orgs and bad signatures get the same 401; bodies over 1 MiB are refused |
Workflow Templates (v0.29.9)
Section titled “Workflow Templates (v0.29.9)”Record-only workflow templates: SessionFS stores templates, starts a review loop from one and computes the plan; your own orchestrator launches every agent. Reads need project read access (org templates: org members); template writes and starting a workflow are Review Loop writes (Team+).
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/projects/{id}/workflow-templates |
Bearer (loops:read) |
Active templates usable in the project: its own plus its org’s |
| POST | /api/v1/projects/{id}/workflow-templates |
Bearer (loops:write, editor) |
Create a project template (name, definition). Validated, DLP-scanned under the strictest policy before storage; at most 100 active templates per scope (409 template_limit) |
| GET | /api/v1/projects/{id}/workflow-templates/{template_id} |
Bearer (loops:read) |
Any version by id: the project’s own, or its org’s when visible |
| PUT | /api/v1/projects/{id}/workflow-templates/{template_id} |
Bearer (loops:write, editor) |
Save a new version; the old one is deactivated and started loops keep their pinned version |
| DELETE | /api/v1/projects/{id}/workflow-templates/{template_id} |
Bearer (loops:write, editor) |
Deactivate a version so it can no longer be started |
| GET, POST | /api/v1/orgs/{org_id}/workflow-templates |
Bearer (org member to read, org admin to write) | The org’s templates, usable in the org’s projects. Service keys limited to some projects read org templates only through those projects |
| GET, PUT, DELETE | /api/v1/orgs/{org_id}/workflow-templates/{template_id} |
Bearer (org member to read, org admin to write) | Read, version or deactivate an org template |
| POST | /api/v1/projects/{id}/workflows |
Bearer (loops:write, Team+) |
Start a workflow from a template (template_id) on an issue (ticket_id) or a new one (title): a review loop pinned to the template version with one child ticket per step slot. Same budgets and rules as starting a loop |
| GET | /api/v1/projects/{id}/workflows/{loop_id}/plan |
Bearer (loops:read) |
Each step’s state (blocked, ready, running, needs_human, done), its gate and the reason it is not ready, plus the ready child tickets with their agent and rendered brief |
The worker-run, finish and review-round routes above take an optional step_id; a report that does not match the pinned template is 422 workflow_step_mismatch.
Meetings (v0.29.0)
Section titled “Meetings (v0.29.0)”Stored meeting transcripts (GitHub #116). A meeting belongs to exactly one project or one group; project meetings admit service keys via meetings:read / meetings:write, while group meetings require a user key (Team+). Uploads are DLP-scanned under the strictest union of the caller’s orgs and the scope org; a block-mode policy rejects with 422 dlp_blocked, and every other mode (redact, warn, or no policy) redacts secrets before storage. See Meetings.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/projects/{id}/meetings |
Bearer (meetings:write) |
Upload a meeting (body: title, content, optional meeting_at, duration_minutes, source). Hard caps: 1 MiB request body, 512 KiB / 10000 lines of normalised content (else 413 meeting_too_large) |
| GET | /api/v1/projects/{id}/meetings |
Bearer (meetings:read) |
List project meetings newest first. Query: q (substring over title and content) |
| POST | /api/v1/groups/{group_id}/meetings |
Bearer (user key, group editor/admin) | Upload a group meeting (same body and caps) |
| GET | /api/v1/groups/{group_id}/meetings |
Bearer (user key, group reader+) | List group meetings newest first. Query: q |
| GET | /api/v1/meetings/{id} |
Bearer (meetings:read) |
Get a meeting with its content, participants and action items |
| DELETE | /api/v1/meetings/{id} |
Bearer (meetings:write) |
Soft-delete a meeting (the uploader, a project admin, or a group admin) |
| PUT | /api/v1/meetings/{id}/participants/{participant_id} |
Bearer (user key) | Manually override a participant’s resolution (body: exactly one of user_id, persona_name or contact_id) |
| POST | /api/v1/meetings/{id}/participants/resolve-bulk |
Bearer (user key) | Override up to 100 participants in one all-or-nothing request ({"participants": [{"participant_id", ...one target}]}); a refused item gives 422 bulk_item_failed with details.results. ?kind=chat for chat threads |
| POST | /api/v1/meetings/{id}/extract |
Bearer (meetings:write) |
Extract participants and action items from the transcript (one LLM call; returns dropped_items). 409 llm_key_required with no key, 429 helper_budget_exceeded over the org budget |
| POST | /api/v1/meetings/{id}/action-items/{item_id}/promote/preview |
Bearer (meetings:write) |
Draft a ticket from a pending action item (project meeting: target is its project; group meeting: returns editable target_project_options) |
| POST | /api/v1/meetings/{id}/action-items/{item_id}/promote |
Bearer (meetings:write, agent_tickets) |
Promote a pending action item to a ticket (body: title, description, priority, kind, optional assigned_to / assigned_user_id / due_date / target_project_id / publish_to_jira). 409 item_not_pending on a concurrent promote |
| POST | /api/v1/meetings/{id}/action-items/{item_id}/dismiss |
Bearer (meetings:write) |
Dismiss a pending action item (409 item_not_pending if not pending) |
| POST | /api/v1/meetings/{id}/action-items/{item_id}/restore |
Bearer (meetings:write) |
Restore a dismissed action item (409 item_not_dismissed if not dismissed) |
Organizations
Section titled “Organizations”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/org |
Bearer | Create organization (Team tier or above required) |
| GET | /api/v1/org |
Bearer | Get org info, member list, and current user role |
| POST | /api/v1/org/invite |
Bearer | Invite a member by email (admin only). The response’s email_sent says whether an email went out and accept_url is the link to share when it didn’t. Sends invite email best-effort (v0.10.22). UPSERTs over stale (org_id, email) rows (declined / expired / orphan-accepted) since v0.10.28 — regenerates id to invalidate stale acceptance links and preserves created_at as the audit signal. |
| POST | /api/v1/org/invite/{invite_id}/accept |
Bearer | Accept an org invite |
| POST | /api/v1/org/invite/{invite_id}/decline |
Bearer | Decline a pending invite (recipient only, optional decline_reason) — v0.10.22 |
| GET | /api/v1/org/invites |
Bearer | List pending invites (admin only) |
| GET | /api/v1/org/invites/me |
Bearer | List pending invites addressed to the caller (v0.10.22) |
| DELETE | /api/v1/org/invites/{invite_id} |
Bearer | Revoke a pending invite (admin only) |
| POST | /api/v1/orgs/{org_id}/invites/{invite_id}/resend |
Bearer | Resend an invite email (admin only) — v0.10.22 |
| POST | /api/v1/orgs/{org_id}/invites/bulk |
Bearer (user key) | Invite up to 100 emails in one all-or-nothing request (admin only). Body {"invites": [{"email", "role"}]}; duplicates refused (422 duplicate_items), more than 100 refused (422 too_many_items), any refused item rolls back the batch (422 bulk_item_failed with details.results). Seat check counts members plus the whole batch. Each result carries invite_id, email_sent and accept_url |
| POST | /api/v1/orgs/{org_id}/invite |
Bearer | Org-scoped invite path (admin only). Same UPSERT-over-stale semantics as /api/v1/org/invite since v0.10.28. |
| PUT | /api/v1/org/members/{user_id}/role |
Bearer | Change a member’s role (admin only) |
| DELETE | /api/v1/org/members/{user_id} |
Bearer | Remove a member from the org (admin only) |
| PUT | /api/v1/orgs/{org_id}/settings |
Bearer | Update org settings (admin only). Merges since v0.23.0: settings left out are kept, an explicit null restores the default. Includes helper_retention_days and meeting_retention_days (both 1–3650) |
| POST | /api/v1/orgs/{org_id}/projects/{project_id}/propose-transfer |
Bearer | Propose moving a member’s personal project into the org (owner or admin; the project owner must accept) — v0.23.0 |
| PUT | /api/v1/orgs/{org_id}/members/me/digest |
Bearer | Turn your weekly activity digest off or on ({"opt_out": true}) — v0.23.0 |
| GET / PUT / DELETE | /api/v1/orgs/{org_id}/llm-settings |
Bearer | Org AI key for the session helper (owner or admin, Enterprise) — v0.23.0 |
| POST | /api/v1/orgs/{org_id}/llm-settings/test |
Bearer | Test the org AI key connection (owner or admin, Enterprise); same body and response as /settings/judge/test |
| POST | /api/v1/orgs/{org_id}/llm-settings/models |
Bearer | List the models the org key can use (owner or admin, Enterprise); same body and response as /settings/judge/models, rate-limited per user (429 models_rate_limited) — v0.28.0 |
| GET / PUT | /api/v1/orgs/{org_id}/helper-settings |
Bearer | Org helper settings, ceilings, instructions and monthly token budget (PUT: owner or admin, Enterprise; GET: members see effective values and their own usage) |
| GET / PUT / DELETE | /api/v1/orgs/{org_id}/integrations/jira |
Bearer | Jira Cloud connection (admin, Enterprise); also /projects, /mappings[/{project_id}], /health, /sync (two-way sync settings and, since v0.28.0, the sprint and epic-link field overrides) — v0.23.0 |
| POST | /api/v1/orgs/{org_id}/jira/fields/detect |
Bearer | Detect the Jira site’s sprint and epic-link field ids without saving them (owner or admin, Enterprise) — v0.28.0 |
| GET | /api/v1/orgs/{org_id}/personas/{name}/rollup |
Bearer | Org persona activity across the projects you can see — v0.23.0 |
| GET | /api/v1/orgs/{org_id}/personas/{name}/represent-history |
Bearer | Who has represented an org persona over time (admin) — v0.23.0 |
Project Groups
Section titled “Project Groups”Project groups are org-scoped flat collections of projects (Team+). Group roles are reader (read), editor (edit) and admin (edit + manage the group); no group role grants project-admin standing. Org admins/owners have admin standing on every group in their org.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/orgs/{org_id}/groups |
Bearer | List the org’s live groups (org admins see all; other members see the groups they hold a grant on; non-members 404) |
| POST | /api/v1/orgs/{org_id}/groups |
Bearer | Create a group (org admin). Body: name, optional slug, description. 409 on duplicate slug |
| GET | /api/v1/orgs/{org_id}/groups/{group_id} |
Bearer | Group detail with members and projects (org admin or a member granted on the group) |
| PATCH | /api/v1/orgs/{org_id}/groups/{group_id} |
Bearer | Rename / re-slug / re-describe (group admin) |
| DELETE | /api/v1/orgs/{org_id}/groups/{group_id} |
Bearer | Soft-delete the group, freeing its slug (org admin) |
| PUT | /api/v1/orgs/{org_id}/groups/{group_id}/members/{user_id} |
Bearer | Grant or update a member’s role (group admin). Grantee must be an org member, else 422 not_org_member |
| POST | /api/v1/orgs/{org_id}/groups/{group_id}/members/bulk |
Bearer (user key) | Grant or update up to 100 members ({"grants": [{"user_id", "role"}]}), all or nothing, same rules as the single grant; a refused item gives 422 bulk_item_failed with details.results |
| DELETE | /api/v1/orgs/{org_id}/groups/{group_id}/members/{user_id} |
Bearer | Revoke a member’s grant (group admin) |
| PUT | /api/v1/orgs/{org_id}/groups/{group_id}/projects/{project_id} |
Bearer | Add a project to the group (group admin). Project must be in this org, else 422 project_not_in_org; a non-org-admin needs edit standing on it, and an unreadable project is 404 |
| DELETE | /api/v1/orgs/{org_id}/groups/{group_id}/projects/{project_id} |
Bearer | Remove a project from the group (group admin) |
| GET | /api/v1/projects/{project_id}/groups |
Bearer | The live groups a project belongs to (project reader; empty unless an org member) |
| GET | /api/v1/me/groups |
Bearer | The caller’s groups across orgs |
Restricted project visibility is toggled through the org settings surface (PUT /api/v1/orgs/{org_id}/settings, {"restrict_project_visibility": true}) and turning it on requires the project_groups feature (Team+). Creating groups, granting members and adding projects also need the feature; reads and cleanup (revoke, remove project, delete) do not.
Skills Marketplace
Section titled “Skills Marketplace”All package versions are immutable and addressed by content_hash. Visibility failures return 404 so callers cannot probe private user or organization listings.
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/marketplace/packages |
marketplace:read |
Search authorized listings (q, tool, limit ≤ 100, opaque cursor); keyset-paginated — the response carries count (page size), has_more and next_cursor. Discovery filters, all applied before the page is cut: category, publisher (namespace or id), verified, attested (trusted-publishing provenance), license (none = undeclared), max_severity (none…critical, worst open scan finding), capability (instructions_only, scripts, mcp, hooks, executables, allowed_tools), works_in (tool; only a check of the package’s current version counts — a new release reads as not yet verified), updated_within (days), include_deprecated, and sort (recent default, or relevance: not deprecated, text match, verified publisher, works-in coverage, log-scale 30-day installs, current version, newest). Each item adds categories, deprecation (with the successor only when you can see it), freshness (stale, unmaintained, last updated / last verified per tool) and installs (30 days, by tool, counts under 10 as {"count": null, "display": "<10"}; a total whose breakdown hides a part is rounded down to a multiple of 10 with "approximate": true, e.g. "40+") |
| GET | /api/v1/marketplace/categories |
marketplace:read |
The curated category taxonomy |
| GET | /api/v1/marketplace/packages/{package_id}/readme |
marketplace:read |
The current (or ?version_id=) version’s README, else the first SKILL.md body, as untrusted markdown: hidden and bidirectional characters replaced by ⟦U+XXXX⟧ and listed in hidden_unicode (line, column, code point, kind). Render without raw HTML. Archive reads are streamed (only the wanted file is read), cached per version, and limited per server process: 503 discovery_busy with Retry-After when saturated |
| GET | /api/v1/marketplace/versions/{version_id}/assets?path= |
marketplace:read |
A PNG, JPEG, GIF or WebP from inside the package (≤ 1 MiB, extension and content must agree; never SVG), with nosniff and a sandbox CSP. Same visibility as a download; cached (including misses) and admission-limited like the README (503 discovery_busy) |
| POST | /api/v1/marketplace/versions/{version_id}/events |
marketplace:read |
Count an install-lifecycle event: {"event": "install"|"uninstall"|"active"|"translation_failure", "tools": [...], "source": "download"|"reconcile"|"native", "project_id"?} → 202 {"counted": n}. One count per principal per version, tool, event and day; daily aggregates only. An uninstall of a version you can no longer see is accepted and not counted |
| GET | /api/v1/publishers/{publisher_id}/analytics?days=7|30|90 |
marketplace:read |
Publisher managers only (Starter+ personal, Team+ org; any other window is 422 invalid_window): installs by tool, version and day, active installs, uninstall rate (only with ≥ 10 installs), translation failures, update adoption, works-in regressions, scan findings, open reports, each package’s settings. Counts under 10 show as <10; if any finest cell (package × version × tool × day) in the window is under 10, every count of that event in the response is rounded down to a multiple of 10 ("approximate": true, e.g. "40+"), and listing install counts follow the same rule over the last 30 days, so no hidden count can be recovered by differencing totals, breakdowns, windows or the listing; consuming orgs never shown |
| PATCH | /api/v1/publishers/{publisher_id}/packages/{package_id} |
marketplace:write |
Publisher managers: categories (≤ 3), deprecated, deprecation_message, successor_package_id (must belong to the same publisher, else 422 successor_not_owned), readme_path. deprecation_message must be one line (≤ 500) without control, zero-width or bidirectional characters (422) |
| GET | /api/v1/orgs/{org_id}/skills/usage?days= |
marketplace:read |
Org owners/admins, Team+: the org’s own members’ installs, active installs, uninstalls and translation failures by package and tool, with stale / deprecated / unmaintained flags |
| POST | /api/v1/admin/marketplace/freshness/sweep |
platform admin or platform_sweeps:run |
Daily freshness sweeper (limit, cursor, time_budget_seconds): stale / unmaintained flags, last updated per tool, ranking inputs, and pruning of dedupe rows older than 30 days. Resumable: pass next_cursor until done |
| POST | /api/v1/admin/marketplace/works-in-results · /source-status |
platform admin | Intake for the sandboxed works-in harness (results: [{version_id, tool, passed, checked_at?, detail?}]; only the package’s current version counts, and a failure after a pass is a regression) and for the source-repository archive check (items: [{package_id, archived}]) |
| GET | /api/v1/marketplace/packages/{package_id} |
marketplace:read |
Inspect provenance, risk, compatibility, license, and current immutable version; owner_org_id is the owning org (null for personal and system packages) |
| GET | /api/v1/marketplace/versions/{version_id}/preview |
marketplace:read |
Bounded untrusted instruction preview; no consent side effect |
| GET | /api/v1/marketplace/versions/{version_id}/download |
marketplace:read |
Download the canonical archive with its hash in X-SessionFS-Content-Hash |
| GET | /api/v1/marketplace/feeds/{tool} |
marketplace:read |
Target-aware authenticated download-and-translate plan (limit, cursor; returns count, has_more, next_cursor); not a claim of native vendor installation |
| POST | /api/v1/users/me/skills/packages |
marketplace:write |
Publish a private DLP-scanned package upload. An optional source_locator is stored and returned in canonical form; a password in any scheme, a username (other than git@ on ssh), a query string or a fragment returns 422 invalid_source_locator |
| GET/POST | /api/v1/users/me/skills/registries |
marketplace:read / marketplace:write |
List or register user marketplace references (user keys only — service keys are refused); SessionFS does not server-fetch them |
| GET/POST | /api/v1/orgs/{org_id}/skills/registries |
skills:read / skills:admin |
List or register organization marketplace references. An https:// registry is polled by the server every 900 s by default (poll_interval_seconds, minimum 300, 0 = metadata-only; at most 10 polling registries per org by default, 409 poll_registry_limit beyond); the listing carries last_poll_at, last_poll_status, last_poll_error_summary, consecutive_failures, next_poll_at and backoff |
| PATCH | /api/v1/orgs/{org_id}/skills/registries/{registry_id} |
skills:admin |
Owner/admin: change policy_state and the polling settings (poll_interval_seconds, tag_pattern, branch_only_mode + branch, credential_secret_ref as env:/k8s: reference, publish_namespace). Another org’s registry is a 404 |
| POST | /api/v1/orgs/{org_id}/skills/registries/{registry_id}/poll |
skills:admin |
“Poll now”: schedules the registry for the next poller run (never fetches in the request). At most once a minute per registry (429 + Retry-After) |
| GET | /api/v1/orgs/{org_id}/skills/registries/{registry_id}/poll-refs |
skills:read |
The poller’s per-ref history: ref, commit, outcome (published / partial / rejected / skipped), version ids and per-package refusal codes |
| POST | /api/v1/admin/skills/registries/poll |
skills_registry:poll |
Scheduler: poll due registries (bounded batch, leased per registry). A skills_registry:poll service key minted by a platform admin, or a platform-admin user key |
| POST | /api/v1/orgs/{org_id}/skills/packages |
skills:admin |
Publish an organization or platform-curated package |
| GET | /api/v1/marketplace/projects/resolve |
skills:read |
Resolve ?git_remote=<normalized remote> to {"project_id","name"} only. Service keys are admitted and bounded by their org and project allowlist; unknown remote or no access is a constant 404 Project not found |
| POST | /api/v1/projects/{project_id}/skill-install-requests |
skills:request |
Request exact-version instruction access |
| GET | /api/v1/projects/{project_id}/skill-install-requests |
skills:read |
List project requests. Response carries incomplete and omitted_count (see the note below) |
| GET | /api/v1/projects/{project_id}/skill-install-requests/{id} |
skills:read |
Inspect one request with its risk summary and challenge state |
| DELETE | /api/v1/users/me/skills/packages/{package_id}/versions/{version_id} |
marketplace:write |
Owner-only deletion of one immutable personal version (frees the 50-version quota); 409 version_referenced while an active org assignment or approved install request still references it (revoked ones are kept as history and do not block), and 409 active_block while the version, its package or its bytes are blocked (a moderator must restore it first; yanks do not block deletion). Deleting accounts, orgs or versions never lifts a block |
| DELETE | /api/v1/orgs/{org_id}/skills/packages/{package_id}/versions/{version_id} |
skills:admin |
Same for organization packages (org admin, audited) |
| POST | /api/v1/projects/{project_id}/skill-install-requests/{id}/challenge |
skills:approve |
Requester-only: emails a single-use 10-minute confirmation code (sha256 at rest; 5 wrong attempts invalidate it) required to approve one’s own request |
| POST | /api/v1/projects/{project_id}/skill-install-requests/{id}/approve |
skills:approve |
Owner/admin atomic approval with consent receipts. Body: approved_capabilities (every requested elevated capability must be listed) and, when the approver is the requester, confirmation_code — otherwise 403 self_approval_requires_confirmation |
| POST | /api/v1/projects/{project_id}/skill-install-requests/{id}/cancel |
skills:request |
Requester/admin atomic cancellation |
| POST | /api/v1/projects/{project_id}/skill-install-requests/{id}/revoke |
skills:approve |
Requester or project admin revokes an approved/applied grant (→ revoked); consent receipts are invalidated; device state is not touched (warning on applied). Frees the version for deletion. Not available over MCP |
| GET | /api/v1/marketplace/versions/{version_id}/instructions |
skills:read |
Load approved instruction-only content; executable components are omitted |
| GET/POST | /api/v1/orgs/{org_id}/skills/assignments |
skills:read / skills:admin |
List or create exact-version managed assignments. The list carries incomplete and omitted_count |
| DELETE | /api/v1/orgs/{org_id}/skills/assignments/{assignment_id} |
skills:admin |
Remove an active managed assignment (audited); frees the version for deletion. An assignment revoked by moderation is kept as history: 409 assignment_revoked |
| GET | /api/v1/users/me/skills/assignments |
skills:read |
Resolve effective org/user/device assignments for the caller. Carries incomplete and omitted_count |
| POST | /api/v1/marketplace/packages/{package_id}/reports |
marketplace:write |
Report a listing you can see. Body: reason (malicious, secrets, impersonation, broken, license, spam, other), optional details (≤ 2000 chars, control characters stripped) and version_id. One open report per package per user (409 report_already_open); rate limited (429 report_rate_limited). User keys only |
| GET | /api/v1/marketplace/reports |
skills:admin |
Moderation queue (status = open/actioned/dismissed/all, limit, cursor). Each report is bound at creation to the listings its reporter could see: reports touching any public or personal listing go to platform admins; reports seen only through one org’s listings go to that org’s owners and admins. Listing and dismissing enforce that bound scope. User keys only |
| POST | /api/v1/marketplace/reports/{report_id}/dismiss |
skills:admin |
Dismiss an open report with a required note |
| POST | /api/v1/marketplace/versions/{version_id}/yank · /block · /restore |
skills:admin |
Take down or restore one version (required reason). Two layers: platform admins moderate version-wide (every listing, including public ones) and block the bytes everywhere; org owners/admins (or a personal package’s owner) moderate only their own non-public listings and never a public listing; personal owners may yank but not block. A platform takedown can only be restored by a platform admin; a platform admin may also lift an owner takedown. A restore lifts only the blocks this target itself placed: a target blocked only because a block on identical content elsewhere applies to it returns 409 blocked_by_other_target (restore that block instead). Taken-down listings are refused everywhere (404). Blocking also revokes install approvals, consent receipts and org assignments of the affected copies (marked, not deleted; a restore does not re-grant) and adds the bytes to a durable content-hash blocklist that survives version deletion: blocked bytes are refused at publish (403 content_blocked) and on every delivery path. A platform block applies everywhere; an org block applies only to that org’s non-public listings. User keys only |
| POST | /api/v1/marketplace/packages/{package_id}/yank · /block · /restore |
skills:admin |
Same for a whole package, including versions published later (an owner’s package action covers only the package’s non-public listings). While a package is platform-blocked, publishing into it returns 403 package_blocked and none of its versions can be deleted |
| GET | /api/v1/marketplace/revocations |
skills:read |
Revocations feed: ?since=<iso>&limit= (≤ 500) → {"revocations":[{"package_id","version_id","content_hash","state","reason","changed_at","kind","block_scope","scope_org_id"}],"next_since":"<iso>","has_more":bool}, oldest first. Echo next_since back as since; event timestamps are unique and strictly increasing, so paging never skips or repeats. kind is version, package (version_id/content_hash null) or content (a blocked or unblocked content hash; package_id/version_id null). block_scope is global for a platform block (or its restore), org for an org owner/admin’s block (or its restore), owner for an owner-layer yank (or its restore) by an org owner/admin or a personal package’s owner, and null for a platform yank (or its restore). org and owner entries apply only to copies installed from a non-public listing of that package/version; an owner never takes down a public listing (only a platform admin does). scope_org_id is the owning org for org (always set) and owner entries (null for a personal package), and null otherwise; when set, also require the copy’s owner_org_id to match. Version and package events are scoped to listings you can see; a global content block reaches every caller, with a generic reason unless you can see the moderated listing; an org content block reaches that org’s members |
Grant list pages and incompleteness. The install-request list and both assignment lists are keyset-paginated: pass limit (1–500, default 100) and cursor (the previous page’s next_cursor). Every response is {"items": [...], "incomplete": bool, "omitted_count": int, "has_more": bool, "next_cursor": str|null, "generation": str} (the install-request list also carries can_approve). Pages are in a stable id order; keep requesting with cursor=next_cursor until has_more is false - only then is the set complete. A page can hold fewer than limit items (filters and omitted records). An invalid cursor is a 422. generation is an opaque token for the collection’s state, carried inside every cursor: if anything in the collection changes while you page (a grant created, decided, revoked or deleted, a membership change for the caller’s assignment list, or a moderation action affecting one of its versions), the next page returns 409 listing_changed - restart from the first page. The lists never guess a grant’s targeting: a stored record whose target_tools or requested_capabilities is not a list of known names is omitted from its page, and that page reports incomplete: true with the count in omitted_count; treat the whole set as incomplete if any page was. Fetching or approving such a record directly returns 500 grant_record_corrupt, and it never grants anything.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/admin/users/{user_id}/api-keys |
Bearer (admin) | Mint a user-kind ApiKey on behalf of any active user as a lost-key recovery path. Raw key returned exactly once; audit row via _log_action(action="mint_api_key_on_behalf", details={key_id, name}). Guards: 404 unknown user, 403 inactive user (no backdoor for disabled accounts), 403 non-admin caller. Service keys are NOT minted here — they remain at /api/v1/orgs/{org_id}/service-keys per v0.10.10. (v0.10.28) |
| GET | /api/v1/admin/metrics |
Bearer (admin or admin_metrics:read) |
Prometheus metrics: request latency by route, push outcomes and sizes, pulls, handoffs, DLP findings, auth failures, active users, sessions, DB pool (v0.23.0) |
| POST | /api/v1/admin/sweeps/kb-compile |
Bearer (admin or platform_sweeps:run) |
Compile projects whose schedule or claims threshold is due (v0.23.0) |
| POST | /api/v1/admin/sweeps/org-digest |
Bearer (admin or platform_sweeps:run) |
Send the weekly org activity digest, once per org per week (v0.23.0) |
| POST | /api/v1/admin/sweeps/helper-retention |
Bearer (admin or platform_sweeps:run) |
Delete expired session helper messages (v0.23.0) and token-usage ledger rows older than 400 days |
| POST | /api/v1/admin/sweeps/ticket-notifications |
Bearer (admin or platform_sweeps:run) |
Send the ticket notification email digest (v0.24.0) |
| POST | /api/v1/admin/sweeps/attention-hides |
Bearer (admin or platform_sweeps:run) |
Delete For You hides older than 90 days (v0.29.8) |
| POST | /api/v1/admin/sweeps/idempotency-keys |
Bearer (admin or platform_sweeps:run) |
Delete stored Idempotency-Key responses older than 24 hours (v0.30.0) |
| POST | /api/v1/admin/projects/{project_id}/restore-from-compilation |
Bearer (admin) | Restore project.context_document from a prior ContextCompilation.context_after; restore compiled_at on participating entries. Body: {compilation_id: int, dry_run: bool = true}. Single atomic transaction. Audit-logged. (v0.10.13) |
Billing
Section titled “Billing”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/billing/status |
Bearer | Current subscription status, tier, storage usage |
| POST | /api/v1/billing/checkout |
Bearer | Start a SessionFS Cloud plan checkout (body: tier, seats) |
| POST | /api/v1/billing/portal |
Bearer | Open the SessionFS Cloud subscription page |
Health
Section titled “Health”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
None | Health check |
| GET | /health/ready |
None | Readiness: 200 {"status":"ready","schema":"current"} (or "ahead") once the database schema is migrated for the running version, else 503 {"status":"not_ready","schema":"behind"} (or "missing" / "unknown") (used by the Helm readiness probe) |
OpenAPI
Section titled “OpenAPI”After deployment, interactive API docs are available at:
- Swagger UI:
https://your-domain/api/docs - ReDoc:
https://your-domain/api/redoc