Skip to content

REST API

The SessionFS API server exposes a REST API for session management, auditing, sync, and settings.

  • Cloud: https://api.sessionfs.dev
  • Self-hosted: Your deployment URL

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 to scopes=["*"].
  • Service keys (v0.10.10+) — created via POST /api/v1/orgs/{org_id}/service-keys. Org-scoped, expirable, and restricted to an enumerated scopes list (e.g. ["handoffs:write", "agent_runs:write"]). Deny-by-default: a service key only reaches a route handler that explicitly opted in via require_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.

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/tickets
Authorization: Bearer sk_sfs_...
Idempotency-Key: 6f1c2f0e-4a51-4f6e-9a0e-2b8b3f7d9c11
Content-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, ETag and Location headers, plus Idempotent-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:

  • 2xx responses and most 4xx responses 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: Bearer credential, 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.
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)

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).

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
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
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)
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
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)
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)
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)

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

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.

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)
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 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.

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)
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
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)

After deployment, interactive API docs are available at:

  • Swagger UI: https://your-domain/api/docs
  • ReDoc: https://your-domain/api/redoc