Skip to content

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 routes

Route groups below:

GroupAuth keyAuthority
/v1/projects /v1/milestones /v1/modules /v1/tasks /v1/roadmap /v1/progress /v1/comments /v1/tasks/:id /v1/tasks/:id/blockersmember key (or platform key + X-Workspace-Id)workspace role + project role
/v1/members /v1/keysmember key (admin/owner role)manage members + rotate/revoke keys
/v1/workspacesplatform key onlytenancy (member keys → 403)
/v1/admin/*member keyadmin/owner workspace role; cross-workspace operator routes
/v1/health, /v1/early-accesspublic

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.

FieldTypeNotes
namestring2–120 chars
emailstringvalid email, ≤200 chars
sourcestringoptional, 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

  • status enum: 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

  • status enum: backlog | todo | in_progress | done

GET /v1/tasks

Member key. Query filters:

ParamType
project_iduuid
module_iduuid
milestone_iduuid
statusone of the four above
assigneeme or a principal uuid
orderdue (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.rolecommenter 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 }
  • type is required (human|agent|app); no silent default.
  • workspace_role limited to member|admin — owner seats are seeded, never API-created.
  • Auto-key: agent/app members get a key minted in the same response ({ key, key_id } shown once) unless create_key:false. human members get no key unless create_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.

MethodPathBodyPurpose
GET/v1/admin/workspacesList all workspaces (id, name, slug, created_at)
POST/v1/admin/workspaces{ name, slug? }Create workspace, idempotent on slug
GET/v1/admin/workspaces/:id/membersMembers of any workspace (read-only)
GET/v1/admin/workspaces/:id/keysKeys 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/rotateRotate across workspaces
POST/v1/admin/keys/:id/revokeRevoke 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).