GET/api/v1/project-templates
GET /api/project-templates
Responses
Security: bearer
POST/api/v1/project-templates
POST /api/project-templates — stores a blueprint (spec validated);
emits `project.template_updated`.
Request body
CreateTemplateRequest
Responses
| Status | Description |
|---|
200 | |
409 | template name already in use |
422 | invalid template spec |
Security: bearer
PATCH/api/v1/project-templates/{id}
PATCH /api/project-templates/{id} — emits `project.template_updated`.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PatchTemplateRequest
Responses
| Status | Description |
|---|
200 | |
404 | template not visible in scope (rls_sparse) |
422 | invalid template spec |
Security: bearer
DELETE/api/v1/project-templates/{id}
DELETE /api/project-templates/{id} — blueprints are data, not live
references: projects already instantiated are untouched; emits
`project.template_removed`.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | template not visible in scope (rls_sparse) |
Security: bearer
POST/api/v1/project-templates/{id}/instantiate
POST /api/project-templates/{id}/instantiate — expands the template
into a REAL project: phases, tasks, dependencies, and every lifecycle
event, all in ONE transaction (claim → expand → insert → events →
record). Rollback on any failure leaves zero partial rows.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
InstantiateTemplateRequest
Responses
| Status | Description |
|---|
200 | |
404 | template not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | invalid template spec or request |
Security: bearer
GET/api/v1/project-views
GET /api/project-views — one project's views, or all org views when
`project_id` is omitted.
Parameters
| Name | In | Type | Required | Description |
|---|
project_id | query | — | No | Scope to one project's views; omit for the org's ENTIRE view set. |
Responses
Security: bearer
POST/api/v1/project-views
POST /api/project-views — saves a view; emits `project.view_updated`.
Request body
CreateViewRequest
Responses
| Status | Description |
|---|
200 | |
404 | project not visible in scope (rls_sparse) |
409 | view name already in use in the scope |
422 | validation failed |
Security: bearer
PATCH/api/v1/project-views/{id}
PATCH /api/project-views/{id} — renames / respecs a view; emits
`project.view_updated`.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PatchViewRequest
Responses
| Status | Description |
|---|
200 | |
404 | view not visible in scope (rls_sparse) |
422 | validation failed |
Security: bearer
DELETE/api/v1/project-views/{id}
DELETE /api/project-views/{id} — emits `project.view_removed`.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | view not visible in scope (rls_sparse) |
Security: bearer
GET/api/v1/projects
GET /api/projects — filtered, newest-first list.
Parameters
| Name | In | Type | Required | Description |
|---|
status | query | null | ProjectStatus | No | |
client_id | query | — | No | |
limit | query | integer | No | |
offset | query | integer | No | |
Responses
Security: bearer
POST/api/v1/projects
POST /api/projects — creates the project and emits `project.created` in
one transaction. Instant-first (IF1/IF2): idempotency-keyed,
version-gated, read-your-write response.
Request body
CreateProjectRequest
Responses
| Status | Description |
|---|
200 | |
409 | version conflict or idempotency key reused |
422 | validation failed |
Security: bearer
GET/api/v1/projects/{id}
GET /api/projects/{id}
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not found |
Security: bearer
PATCH/api/v1/projects/{id}
PATCH /api/projects/{id} — tri-state partial update. Instant-first
(IF1/IF2): idempotency-keyed, version-gated, returns prior + new values.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PatchProjectRequest
Responses
| Status | Description |
|---|
200 | |
404 | project not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | validation failed |
Security: bearer
GET/api/v1/projects/{id}/billing-releases
GET /api/projects/{id}/billing-releases — the release ledger: every
staged milestone/fee for this project with its status (`pending`
bills on the next run reaching its window; `billed` carries the
consuming invoice; `cancelled` terminal).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/projects/{id}/billing-releases/{release_id}/cancel
POST /api/projects/{id}/billing-releases/{release_id}/cancel — cancel
a PENDING release (admin correction; terminal). Billed releases are
immutable — they ride an invoice, and invoices correct through credit
notes.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
release_id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | release not visible/already settled (rls_sparse) |
Security: bearer
GET/api/v1/projects/{id}/gantt
GET /api/projects/{id}/gantt — the dated timeline plus its undated
remainder, each phase carrying its budget burn.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
GET/api/v1/projects/{id}/phase-burn
GET /api/projects/{id}/phase-burn — the burn read model (report P3):
consumed approved billable minutes and card-priced value per phase
against the phase's budget, with the typed `none | warn | over`
alert state (`warn` at ≥80%, `over` at ≥100%).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | project not visible in scope (rls_sparse) |
Security: bearer
GET/api/v1/projects/{id}/phases
GET /api/projects/{id}/phases — ordered by sort_order.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/projects/{id}/phases
POST /api/projects/{id}/phases — adds a phase; emits
`project.phase_created`.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
CreatePhaseRequest
Responses
| Status | Description |
|---|
200 | |
404 | project not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | validation failed |
Security: bearer
PATCH/api/v1/projects/{id}/phases/{phase_id}
PATCH /api/projects/{id}/phases/{phase_id} — tri-state partial update.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
phase_id | path | string | Yes | |
Request body
PatchPhaseRequest
Responses
| Status | Description |
|---|
200 | |
404 | phase not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | validation failed |
Security: bearer
POST/api/v1/projects/{id}/phases/{phase_id}/complete
POST /api/projects/{id}/phases/{phase_id}/complete — stamps the phase
completed and, when it is a billing milestone, emits
`project.milestone_completed` in the same transaction (the core-billing
seam). Instant-first: the idempotency key guarantees a retried
completion can never double-emit the billing event.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
phase_id | path | string | Yes | |
Request body
MutationParams
Responses
| Status | Description |
|---|
200 | |
404 | phase not visible in scope (rls_sparse) |
409 | already completed, version conflict, or idempotency key reused |
Security: bearer
GET/api/v1/projects/{id}/tasks
GET /api/projects/{id}/tasks — filtered by phase/status/assignee and
the milestone flag, sort_order-ordered.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
phase_id | query | — | No | |
status | query | null | TaskStatus | No | |
assignee_id | query | — | No | |
milestone | query | — | No | |
limit | query | integer | No | |
offset | query | integer | No | |
Responses
Security: bearer
POST/api/v1/projects/{id}/tasks
POST /api/projects/{id}/tasks — creates the task (optionally with its
dependency set) and emits `project.task_created` in one transaction.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
CreateTaskRequest
Responses
| Status | Description |
|---|
200 | |
404 | project not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | validation failed |
Security: bearer
PATCH/api/v1/projects/{id}/tasks/{task_id}
PATCH /api/projects/{id}/tasks/{task_id} — tri-state partial update.
`status: "completed"` is rejected: completion runs through the complete
endpoint (dependency gating + the billing seam).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
task_id | path | string | Yes | |
Request body
PatchTaskRequest
Responses
| Status | Description |
|---|
200 | |
404 | task not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | validation failed |
Security: bearer
POST/api/v1/projects/{id}/tasks/{task_id}/complete
POST /api/projects/{id}/tasks/{task_id}/complete — the guarded
completion: every finish-to-start dependency must already be completed
(typed 409 `task_dependencies_unsatisfied` naming the blockers), then
`project.task_completed` fires — plus `project.milestone_completed`
when the task is a billing milestone. Instant-first: the idempotency
key makes retried completions replay instead of double-billing.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
task_id | path | string | Yes | |
Request body
MutationParams
Responses
| Status | Description |
|---|
200 | |
404 | task not visible in scope (rls_sparse) |
409 | dependencies unsatisfied, already completed, version conflict, or idempotency key reused |
Security: bearer
GET/api/v1/projects/{id}/tasks/{task_id}/dependencies
GET /api/projects/{id}/tasks/{task_id}/dependencies
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
task_id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not found |
Security: bearer
PUT/api/v1/projects/{id}/tasks/{task_id}/dependencies
PUT /api/projects/{id}/tasks/{task_id}/dependencies — replaces the
whole dependency set, cycle-checked over the project graph BEFORE any
write; emits `project.task_updated`.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
task_id | path | string | Yes | |
Request body
SetDependenciesRequest
Responses
| Status | Description |
|---|
200 | |
404 | task not visible in scope (rls_sparse) |
422 | dependency set rejected (self-dep, cross-project, or cycle) |
Security: bearer