Skip to content

HTTP API

The web UI is a thin client over this JSON API (http://localhost:3000). Ids in paths are UUIDs unless the route says :slug.

Authentication, organizations and teams

All /api/* routes except the ones marked public require a session cookie (unless AUTH_DISABLED=1). Unauthenticated calls return 401.

Method & path Purpose
GET /api/auth/status public: { authEnabled, needsBootstrap, githubLogin }
GET /api/version public: build identity { name, version, commit } (see Version metadata)
POST /api/auth/register public: { email, password, name?, organizationName?, inviteToken?, waitlistToken? }; the first user bootstraps the default organization and team, a registration without an invite starts its own organization, and otherwise an invite, a waitlist join link (waitlistToken, for its email; starts its own organization) or OPEN_REGISTRATION=1 is required. Sets the session cookie
POST /api/auth/login · POST /api/auth/logout public: { email, password, inviteToken? } / clear session
GET /api/oauth/github/authorize?mode=login[&invite=token] public: GitHub sign-in (same OAuth app as the integration)
GET /api/invites/:token public: invite preview (team, role, email)
POST /api/waitlist public: { email, name?, company?, teamSize? } joins the waitlist; the same { ok: true } whether or not the email was already on it; 5 signups per IP per 10 minutes
GET /api/waitlist/join/:token public: join-link preview (email, name, company)
GET /api/admin/waitlist · POST /api/admin/waitlist/:id/invite · DELETE /api/admin/waitlist/:id owners and admins of the default organization: list, issue a join link ({ link }), remove
POST /api/invites/:token/accept Join the team the invite is for (email must match)
GET /api/me · POST /api/me/team Current user, teams, active team and the active organization / switch active team { teamId }
GET /api/teams · POST /api/teams My teams / create a team in the caller's organization (creator becomes owner)
GET /api/teams/:id · PATCH /api/teams/:id Team detail (members, invites, memory) / rename (admin)
GET/PATCH/DELETE /api/teams/:id/members[/:userId] Members; role changes need admin, owner grants need owner; a team keeps ≥ 1 owner
GET/POST /api/teams/:id/invites · DELETE …/invites/:inviteId Invites (admin): { email, role } → { link }
GET/PUT /api/teams/:id/memory Team memory (members edit)
GET/PUT /api/teams/:id/knowledge Team default knowledge scope (admin)
GET/PUT /api/org/memory Memory of the caller's organization, shared by its teams (owners/admins edit)
GET/PUT /api/org/guardrails The organization's AI data guardrails: { policy: { mode, allow } }. mode is off, warn, mask (the default) or strict; allow lists values or /patterns/ never masked. PUT { mode?, allow? } is for owners and admins; 400 for an unknown mode

Authorization: viewers get 403 on any non-GET route (except chat, invites and their own session); project routes return 403 when the project belongs to a team the caller is not a member of. GET /api/projects, /api/board and /api/history are scoped to the active team.

Tenancy: every /api/org/* route, along with provider keys, model routing, OAuth apps, integrations, knowledge, the GitHub catalog, promotions and usage, reads and writes only the caller's organization — the one its active team belongs to. An account without a team gets 403 no_team everywhere except its own session, team creation and invites.

Version metadata

GET /api/version returns build identity only — no user, team or tenant data — and always responds 200 with a well-formed { name, version, commit } body, never throwing, omitting a field, or returning an empty/whitespace value.

Field Resolution order
name package.json name
version SPACES_VERSION (trimmed, non-empty build label) → package.json version → unknown
commit GIT_COMMIT (trimmed, non-empty) → RAILWAY_GIT_COMMIT_SHA (trimmed, platform-injected) → git rev-parse HEAD → unknown

commit: "unknown" is the sentinel for a build produced with no commit metadata; a packaged deployment built with metadata never reports it. Both SPACES_VERSION and GIT_COMMIT are build-time, non-secret variables baked into the image via --build-arg (see .env.example). Managed platforms such as Railway inject the commit SHA automatically as RAILWAY_GIT_COMMIT_SHA; the Dockerfile defaults GIT_COMMIT to that value when an explicit --build-arg GIT_COMMIT is not passed, so a Railway deployment reports the exact deployed commit without any manual configuration. The values are resolved once at boot and are stable across all requests and across restarts of the same artifact.

Projects

Method & path Purpose
GET /api/projects List projects
POST /api/projects Create: { name, description?, repos?, model?, feature? }. Creates the governing workspace and starts onboarding; 409 no_provider_key when no model key is stored
GET /api/projects/:id Detail with repos, integrations and suggestions
PATCH /api/projects/:id Update
POST /api/runs/:id/pause · POST …/resume · POST …/cancel Pause a running run at its next stage boundary (a queued run is held before start), resume a user-paused run, or cancel a queued/running/paused run; each returns the run snapshot
POST /api/runs/:id/feedback Interrupt a running run with feedback: { message } (up to 4,000 characters). Stored as the run's feedback event; the worker holding the run steers it into the agent after its current step (or gives it to the next stage) and records feedback_delivered (when: now or next stage). 409 unless the run is running (a paused run is answered instead)
POST /api/projects/:id/pause · POST …/resume Pause: queued jobs are held, running runs stop before their next stage (pausingRuns, heldJobs) / resume: dispatch continues and user-paused runs are re-queued (resumedRuns)
POST /api/projects/:id/archive · POST …/unarchive Archive: cancels queued, running and paused work (cancelled on the response), hides the project from the board, refuses new runs and jobs, keeps everything / restore
GET /api/projects/:id/deletion-check What deleting would remove (runs, jobs, snapshots, per-repository action), whether the project is archived, the confirmation phrase, and whether the caller may delete
DELETE /api/projects/:id Permanently delete an archived project and everything it owns. Body { "confirm": "delete <project-key>" } (the key is the project code, e.g. delete plat-12; the deletion-check response carries the exact phrase); owner/admin of the project's team; 409 unless archived and idle
GET /api/board?archived=1 Include archived projects (cards carry archivedAt); the default response omits them and reports archivedCount
GET /api/projects/:id/onboarding · POST …/onboarding Onboarding progress / restart
GET /api/projects/:id/suggestions · POST …/suggestions Suggested repositories & work areas / regenerate ({ basis: 'project' | 'plan' })
POST /api/projects/:id/export Export memory, knowledge and manifest into the governing workspace
GET /api/projects/:id/responsibilities Read the six project accountability responsibilities and their explicit, owner-fallback, or unresolved resolution state; owning-team members only
PUT /api/projects/:id/responsibilities/:responsibilityId/assignments Replace ordered assignees with { userIds: string[] }; owning-team owners/admins only; rejects invalid members and empty Owner assignments
POST /api/projects/:id/responsibilities/migrate Idempotently seed/repair standard responsibilities; owning-team owners/admins only

Repositories

Method & path Purpose
POST /api/projects/:id/repos Add { label, kind: 'local' | 'github', localPath?, githubRepo?, isPrimary?, primaryIfFirst? } (primaryIfFirst makes it primary when it is the project's first code repository); GitHub repos clone, learn and set up in the background
PATCH /api/projects/:id/repos/:repoId Edit label / path / owner-name / primary
DELETE /api/projects/:id/repos/:repoId Remove (drops its brief, recomposes memory, prunes knowledge scope)
POST /api/projects/:id/repos/:repoId/clone · …/learn Re-clone / re-learn
GET /api/projects/:slug/plan-repos Repositories the current plan names, matched against registered ones
GET /api/github/repos Repos visible to the connected GitHub account (wizard autocomplete)
GET /api/github/catalog · POST /api/github/catalog/sync Synced repository catalog / force sync

Runs

Method & path Purpose
POST /api/runs Start: { projectId, pipeline, feature?, constitution?, planContext?, checklistDomain?, model?, thinking?, targetRepoId? }
GET /api/runs/:id Snapshot: status, stage, log, timeline, interrupted, queued, rerunnable
GET /api/runs/:id/events Server-sent events stream
POST /api/runs/:id/answer { answer } for a paused run (approve / continue / text); queues a job that reopens the paused session on any worker and continues from the answer. 409 once the run is no longer paused (a second answer)
POST /api/runs/:id/rerun { fromStage?: string | 'start' }; resumes the previous agent session
POST /api/projects/:slug/execute-step Run one stage: { step, force?, feature?, scope? }. Specify needs a feature description of at least four words (400, field: "feature"). scope is the new intent's scope; unset or auto lets the agent choose it from the description
POST /api/projects/:slug/accept Accept a feature whose verification did not pass: { note? }. Records who accepted it, the verification status at the time and the reason in acceptance.md, and the board counts the feature as done. 409 not_verified when there is no report yet, 409 already_passed when verification passed
DELETE /api/projects/:slug/accept Withdraw that acceptance; the feature returns to whatever its verification says
GET /api/projects/:slug/features Every feature, newest first, with current (the active one), status, review and verification outcome, and documents
POST /api/projects/:slug/features/:id/activate Continue an undelivered feature: switches each repository to its branch where safe, then makes it active. Returns features and per-repository branches ({ repo, switched, reason? }, one per repository, including those with no local checkout). 409 project_busy while a run is live
GET /api/projects/:slug/features/:id One intent for review: { intent, documents, events }. intent is its record (status, scope, review, verification, acceptance, delivery, task progress). documents lists every stored document (path, kind, size, who changed it last and when), with the content fetched per document from the artifact route. events is its status history, oldest first. 404 for an unknown or deleted intent
PATCH /api/projects/:slug/features/:id Rename an intent or change its scope: { title?, scope? }. A title (up to 200 characters) rewrites the first heading of its spec.md; the directory and branch keep their names. A scope (bugfix, feature, mvp, improvement, chore, spike or a short custom label) rewrites its **Scope**: line. Returns features, each with its scope. 400 for an empty or too long title, an empty scope or an unknown intent
DELETE /api/projects/:slug/features/:id Delete a feature that is not delivered (its directory under specs/). 409 delivered, 409 project_busy
GET /api/projects/:slug/pull-requests The current feature's open pull requests (githubRepo, number, url, title, draft), found on GitHub by head branch and cached for a minute. Board cards in Implementing and Releasing carry the same list as pullRequests
GET /api/projects/:slug/latest-run · /jobs · /task-tracker Latest run, job queue with run-aware status (each job with lastActivityAt, its run's newest event, and workerHeartbeatAt), tracker
POST /api/projects/:slug/jobs/:jobId/kill Force kill a stuck job: the job, every active job of its run and the run are cancelled (as POST /api/runs/:id/cancel), and each worker holding them is killed. Under the supervisor a fresh worker starts when the project has work; a standalone worker (bun run worker) exits and must be restarted. Returns jobs. Members only

Sub-agents

Method & path Purpose
POST /api/projects/:slug/subagents/run Start parallel workstreams { maxAgents?, model? } (branches + stacked PRs on GitHub repos)
GET …/subagents · GET …/subagents/events Snapshot / stream
POST …/subagents/retry · …/subagents/cancel Retry / cancel

Knowledge and memory

Method & path Purpose
GET /api/knowledge/sources Connected sources
GET /api/knowledge/search?source=&q= · GET /api/knowledge/item?source=&id= Search / fetch across Jira, Confluence, Linear, GitHub
POST /api/projects/:id/sources/import Attach a ticket/doc to a project { source, id }
GET /api/projects/:id/knowledge · PUT …/knowledge Per-project knowledge scope
GET /api/projects/:slug/memory · POST …/memory · POST …/memory/rebuild Memory read / save manual text / rebuild from repositories
GET /api/projects/:slug/context The shared context bundle

Assistant, workers, integrations

Method & path Purpose
POST /api/projects/:slug/chat { message } → { answer, history, actions }; full context + action tools
GET/DELETE /api/projects/:slug/assistant/history Conversation memory
GET /api/workers · GET /api/projects/:id/worker Live workers / the one serving a project
GET /api/integrations · DELETE /api/integrations/:kind Integration status (with credentialsOk) / disconnect
GET /api/oauth/:provider/authorize[?return=/path] · …/callback OAuth flow; return sends the browser back to a page in the app
GET /api/oauth-apps · PUT/DELETE /api/oauth-apps/:provider Provider app credentials and setup state / paste or remove credentials (admin)
GET /api/oauth-apps/github/manifest[?org=name] Page that posts the GitHub App manifest to GitHub (admin)
GET /api/oauth-apps/github/manifest/callback · …/installed GitHub returns here after creating / installing the app
GET /api/board · GET /api/history Board columns / run history (cards carry usage: tokens and cost across the project's runs)
GET /api/projects/:slug/usage Tokens and cost for a project: totals, by stage, by model, by run
GET /api/org/usage?days=30 Organization spend in the window and all time, by project, plus the GitHub actor (app bot or user)