REST API
Base URL: https://pm-api.blarass.com/v1
All routes require authentication unless marked public.
Authorization: Bearer <member or platform key>X-Workspace-Id: <workspace uuid> # platform key only, on workspace-scoped routesRoute groups below:
| Group | Auth key | Authority |
|---|---|---|
/v1/projects /v1/milestones /v1/modules /v1/tasks /v1/roadmap /v1/progress /v1/comments /v1/tasks/:id /v1/tasks/:id/blockers | member key (or platform key + X-Workspace-Id) | workspace role + project role |
/v1/members /v1/keys | member key (admin/owner role) | manage members + rotate/revoke keys |
/v1/workspaces | platform key only | tenancy (member keys → 403) |
/v1/admin/* | member key | admin/owner workspace role; cross-workspace operator routes |
/v1/health, /v1/early-access | public | — |
Health — public
GET /v1/health
Returns { ok: true, service: "blarass-pm" }.
Early access — public
POST /v1/early-access
Signup capture; sources come from the landing page.
| Field | Type | Notes |
|---|---|---|
name | string | 2–120 chars |
email | string | valid email, ≤200 chars |
source | string | optional, default "landing" |
Returns the created row (409 if the email already exists — upsert is idempotent).
Projects — editor/admin rules
GET /v1/projects
Member key. Lists projects visible to the caller: public projects, plus private ones where they hold a membership row or are owner/admin. Each row includes progress_pct.
GET /v1/projects/:id
Member key. Returns { project, progress, my_role }. my_role is the project-membership row (or null).
POST /v1/projects
Admin/owner workspace role required (plain members have no project to be editor-of, so creation is gated higher).
{ "name": "string", "description": "string?", "status": "active?", "visibility": "public?" }visibility must be public|private.
PATCH /v1/projects/:id
Editor of the project or workspace admin/owner.
{ "name": "string?", "description": "string?", "status": "string?", "visibility": "string?" }Changing visibility specifically requires admin/owner.
DELETE /v1/projects/:id
Editor of the project or workspace admin/owner.
Project members (roles on a specific project)
GET /v1/projects/:id/members
Member key — anyone who can read the project.
POST /v1/projects/:id/members
Admin/owner workspace role.
{ "member_id": "uuid", "role": "viewer?", "see_assigned_only": "boolean?" }role is one of viewer|commenter|editor, default viewer. Upserts: an existing row gets its role (and optionally assigned-only flag) updated.
PATCH /v1/projects/:id/members/:member_id
Admin/owner workspace role. Change role and/or see_assigned_only on an existing row.
DELETE /v1/projects/:id/members/:member_id
Admin/owner workspace role.
Milestones
statusenum:planned | in_progress | done
GET /v1/milestones?project_id=
Member key; visibility checked per project. Includes progress_pct.
POST /v1/milestones
Editor on the target project (or admin/owner).
{ "project_id": "uuid", "name": "string", "target_date": "YYYY-MM-DD?", "status": "planned?", "position": "number?" }PATCH /v1/milestones/:id
Editor on the parent project. Update name, target_date, status, position.
Modules
GET /v1/modules?milestone_id=
Member key; visibility checked through the parent milestone’s project.
POST /v1/modules
Editor on the parent project.
{ "milestone_id": "uuid", "name": "string", "position": "number?" }PATCH /v1/modules/:id
Editor on the parent project. Update name, position.
Tasks
statusenum:backlog | todo | in_progress | done
GET /v1/tasks
Member key. Query filters:
| Param | Type |
|---|---|
project_id | uuid |
module_id | uuid |
milestone_id | uuid |
status | one of the four above |
assignee | me or a principal uuid |
order | due (due-dated first, undated last) |
assignee=me is resolved server-side to the caller’s principal id. Memberships carrying see_assigned_only narrow the result automatically.
GET /v1/tasks/:id
Member key. Returns { task, blockers: [ { id, title, status, due_date } ] } — task.blocked is derived (true if any blocker isn’t done). Assigned-only members only see their own assigned task here.
POST /v1/tasks
Editor on the project (or admin/owner).
{ "project_id": "uuid", "title": "string", "milestone_id": "uuid?", "module_id": "uuid?", "description": "string?", "assignee_principal_id": "uuid?", "estimate_days": "number?", "metadata": "object?", "due_date": "YYYY-MM-DD?", "status": "backlog?" }Defaults: status backlog, metadata {}. module_id must belong to milestone_id if both are given.
PATCH /v1/tasks/:id
Editor on the project. Any subset of: status, title, description, estimate_days, module_id, milestone_id, assignee_principal_id, metadata, due_date (null clears). assignee is an alias for assignee_principal_id.
Task blockers (v1.3 dependencies)
task_dependencies(task_id, blocker_task_id) — a task waits on its blockers.
POST /v1/tasks/:id/blockers
Editor on the task’s project.
{ "blocker_task_id": "uuid" }Errors: 400 on self-block or cross-project blocker, 409 on a cycle (checked transitively).
GET /v1/tasks/:id/blockers
Member key, visibility-checked. Returns the blocker list.
DELETE /v1/tasks/:id/blockers/:blocker_task_id
Member key (editor on the project).
Task comments
project_members.role — commenter or editor required (viewer is read-only).
POST /v1/tasks/:id/comments
{ "body": "string" }Append-only; author is the calling principal. With platform key the author is the workspace’s owner seat (audit metadata; needs exactly one owner member).
GET /v1/tasks/:id/comments
Member key, read.
Roadmap
GET /v1/roadmap?project_id=&from=&to=
Member key. Milestones ordered by target_date (undated last), each with total_tasks / done_tasks / progress_pct. from / to are YYYY-MM-DD and filter dated milestones in period.
Progress
GET /v1/progress?project_id=
Member key. Derived roll-up: module → milestone → project (task-count-based).
Members (workspace roster + lifecycle)
Admin/owner workspace role (member role returns 403).
GET /v1/members
Returns { id, workspace_id, type, workspace_role, name, created_at } for the workspace.
POST /v1/members
Admin/owner.
{ "type": "human|agent|app", "name": "string", "workspace_role": "member?", "create_key": null }typeis required (human|agent|app); no silent default.workspace_rolelimited tomember|admin— owner seats are seeded, never API-created.- Auto-key:
agent/appmembers get a key minted in the same response ({ key, key_id }shown once) unlesscreate_key:false.humanmembers get no key unlesscreate_key:true. - Member-key callers without an active key of their own must pass
create_key:true— that rotates their old key away (rotation semantics, old stops working immediately).
PATCH /v1/members/:id
Admin/owner. { "workspace_role": "member|admin" } — owner seat untouchable.
DELETE /v1/members/:id
Admin/owner. Owner-seat delete only with the real owner key (platform-key operator cannot delete owner seats).
Keys
Keys are pure credentials, one active per member.
POST /v1/keys
Admin/owner.
{ "member_id": "uuid", "expires_at": "iso?" }Returns { key: "<raw once>", id, principal_id, status, expires_at, created_at } — 409 if the member still has an active key.
POST /v1/keys/:id/rotate
Self rotation (any member rotates their own key) or admin/owner for anyone. Old key → rotated; new raw secret returned once.
POST /v1/keys/:id/revoke
Self or admin/owner. Marks revoked; the member is left keyless.
GET /v1/keys
Admin/owner → all keys for the workspace; anyone → their own. Metadata only — raw secrets are never stored or re-shown.
Tenancy — platform key only
requirePlatform (env-stored PLATFORM_ADMIN_KEY, hash-compared timing-safe) is a different credential class from member keys. Member keys on these routes → 403 forbidden: platform ops key required for tenancy routes.
GET /v1/workspaces
All workspaces (with member_count, project_count) — admin-panel/console view.
POST /v1/workspaces
{ "name": "string", "slug": "string?", "owner_name": "string" }Slug auto-derived from name (with collision-safe suffix loop) if omitted. Creates workspace + the first owner member + their first key, returned once in the same response:
{ "workspace": { "id", "name", "slug", "created_at" }, "owner": { "id", "type", "workspace_role", "name" }, "key": "<raw once>", "key_id": "<key uuid>" }Rotate-to-retrieve is the re-embed workflow for a lost key.
DELETE /v1/workspaces/:id
Platform key, teardown (ON DELETE CASCADE across all tenant tables).
Admin operator routes (/v1/admin/*)
Admin/owner workspace role required on every admin route. These deliberately cross the single-workspace tenant boundary — the operator console needs it.
| Method | Path | Body | Purpose |
|---|---|---|---|
| GET | /v1/admin/workspaces | — | List all workspaces (id, name, slug, created_at) |
| POST | /v1/admin/workspaces | { name, slug? } | Create workspace, idempotent on slug |
| GET | /v1/admin/workspaces/:id/members | — | Members of any workspace (read-only) |
| GET | /v1/admin/workspaces/:id/keys | — | Keys of any workspace (metadata) |
| POST | /v1/admin/keys | { principal_id, expires_at? } | Mint a key for a principal in any workspace |
| POST | /v1/admin/keys/:id/rotate | — | Rotate across workspaces |
| POST | /v1/admin/keys/:id/revoke | — | Revoke across workspaces |
POST /v1/admin/bootstrap (one-time)
x-bootstrap-token header must equal BOOTSTRAP_TOKEN env. Mints the first admin key on a seeded principal; permanently disabled once any active key exists (403 bootstrap already completed).