Projects API
All endpoints require authentication and projects tool permissions. Routes are mounted at /api/projects.
Data Scoping
Section titled “Data Scoping”| Access Level | Visibility |
|---|---|
employee / lead | Projects where user is owner, delivery manager, or a member |
manager | Own projects + squad members’ projects + direct reports’ projects |
head / executive | All projects |
List Projects
Section titled “List Projects”GET /api/projectsPermission: projects.view
Query parameters:
| Param | Type | Description |
|---|---|---|
company_id | string | Filter by company ID |
status | string | Filter by status (comma-separated, e.g. active,completed) |
type_id | string | Filter by project type config ID (comma-separated) |
owner_id | string | Filter by owner user ID (comma-separated) |
squad | string | Filter by squad name (comma-separated) |
search | string | Search by project name or description |
Response: { projects }
Each project includes joined fields: company_name, owner_name, delivery_manager_name, type_label, type_value, member_count, and task_count.
Get Project Detail
Section titled “Get Project Detail”GET /api/projects/:idPermission: projects.view
Response: { project, members, goals }
The project object includes: company_name, owner_name, owner_picture, delivery_manager_name, type_label, type_value, sla_policy_name, sla_response_hours, sla_resolution_hours.
Members include user_id, name, picture, job_title, and role.
Goals are ordered by position then created_at.
Create Project
Section titled “Create Project”POST /api/projectsPermission: projects.update
Body:
{ "name": "string (required)", "description": "string", "company_id": "string", "type_id": "string", "status": "string (default: active)", "owner_id": "string (default: current user)", "delivery_manager_id": "string", "squad": "string", "start_date": "string (YYYY-MM-DD)", "end_date": "string (YYYY-MM-DD)", "sla_policy_id": "string"}Response: 201 { project }
Side effects:
- Auto-adds the creating user as a member with role
owner - Logs a
createdhistory entry - Auto-populates heatmap cell if
company_idand project type are set - Auto-completes client onboarding milestones for
project_createdtrigger
Update Project
Section titled “Update Project”PUT /api/projects/:idPermission: projects.update
Body: Any subset of the following fields:
| Field | Type | Description |
|---|---|---|
name | string | Project name |
description | string | Project description |
company_id | string | Linked company |
type_id | string | Project type config item ID |
status | string | Project status |
owner_id | string | Owner user ID |
delivery_manager_id | string | Delivery manager user ID |
squad | string | Squad name |
start_date | string | Start date (YYYY-MM-DD) |
end_date | string | End date (YYYY-MM-DD) |
metadata | string | Arbitrary metadata |
sla_policy_id | string | SLA policy ID |
auto_ticket_from_email | string | Auto-ticket email address |
Response: { project }
Side effects:
- Logs an
updatedhistory entry - Auto-populates heatmap cell on status or company change
- When status is set to
completedand project has a company: auto-creates a draft client win and notifies the assigned delivery manager
Delete Project
Section titled “Delete Project”DELETE /api/projects/:idPermission: projects.manage
Response: { ok: true }
Returns 404 if the project does not exist.
Members
Section titled “Members”List Members
Section titled “List Members”GET /api/projects/:id/membersPermission: projects.view
Response: { members }
Each member includes id, role, created_at, user_id, name, picture, and job_title.
Add Member
Section titled “Add Member”POST /api/projects/:id/membersPermission: projects.update
Body:
{ "user_id": "string (required)", "role": "string (default: member)"}Response: 201 { ok: true }
Uses INSERT OR IGNORE so adding an existing member is a no-op. Sends a project_member_added notification to the new member (unless they are the current user).
Remove Member
Section titled “Remove Member”DELETE /api/projects/:id/members/:userIdPermission: projects.update
Response: { ok: true }
List Goals
Section titled “List Goals”GET /api/projects/:id/goalsPermission: projects.view
Response: { goals }
Goals are ordered by position then created_at.
Create Goal
Section titled “Create Goal”POST /api/projects/:id/goalsPermission: projects.update
Body:
{ "title": "string (required)", "description": "string", "due_date": "string (YYYY-MM-DD)", "position": "number (default: 0)"}Response: 201 { goal }
Update Goal
Section titled “Update Goal”PUT /api/projects/:id/goals/:goalIdPermission: projects.update
Body: Any subset of:
| Field | Type | Description |
|---|---|---|
title | string | Goal title |
description | string | Goal description |
status | string | Goal status |
due_date | string | Due date (YYYY-MM-DD) |
position | number | Sort position |
Response: { goal }
Delete Goal
Section titled “Delete Goal”DELETE /api/projects/:id/goals/:goalIdPermission: projects.update
Response: { ok: true }
List Project Tasks
Section titled “List Project Tasks”GET /api/projects/:id/tasksPermission: projects.view
Response: { tasks }
Returns top-level tasks only (parent_task_id IS NULL) linked to this project via resource_type = 'project'. Each task includes joined fields: assignee_name, assignee_picture, status_label, status_color, status_value, priority_label, priority_color.
Tasks are ordered by sort_order, due_date, then created_at DESC.
Decision Log
Section titled “Decision Log”List Decisions
Section titled “List Decisions”GET /api/projects/:id/decisionsPermission: projects.view
Response: { decisions }
Each decision includes decided_by_name. Ordered by decided_at DESC, then created_at DESC.
Create Decision
Section titled “Create Decision”POST /api/projects/:id/decisionsPermission: projects.update
Body:
{ "title": "string (required)", "decision": "string (required)", "rationale": "string", "decided_by": "string (user ID)", "decided_at": "string (ISO date)", "status": "string (default: agreed)"}Response: 201 { decision }
Update Decision
Section titled “Update Decision”PUT /api/projects/:id/decisions/:decisionIdPermission: projects.update
Body: Any subset of: title, decision, rationale, decided_by, decided_at, status.
Response: { decision }
Delete Decision
Section titled “Delete Decision”DELETE /api/projects/:id/decisions/:decisionIdPermission: projects.update
Response: { ok: true }
Profitability
Section titled “Profitability”GET /api/projects/:id/profitabilityPermission: projects.view
Returns a financial and time summary for the project by aggregating budgets, time entries, and invoices.
Response:
{ "budget_total": 0, "total_hours": 0, "billable_hours": 0, "team_size": 0, "invoiced": 0, "collected": 0, "time_by_member": [ { "id": "string", "name": "string", "picture": "string", "total_hours": 0, "billable_hours": 0 } ]}AI Summary
Section titled “AI Summary”Get Cached Summary
Section titled “Get Cached Summary”GET /api/projects/:id/ai-summaryPermission: projects.view
Response: { summary } where summary is null if no summary has been generated, or:
{ "id": "string", "project_id": "string", "summary_text": "string", "recommendations": ["string", "string", "string"], "generated_at": "ISO datetime"}Generate Summary
Section titled “Generate Summary”POST /api/projects/:id/ai-summaryPermission: projects.view
Generates a new AI summary using Workers AI (Kimi K2.5). Gathers project details, members, tasks, and goals to produce a 3-5 sentence summary and 2-3 actionable recommendations. The result is cached in project_ai_summaries (upserted on subsequent calls).
Response: { summary } (same shape as GET)
Returns 503 if the AI binding is not configured. Returns 502 if AI generation fails.
Linked Deal
Section titled “Linked Deal”GET /api/projects/:id/dealPermission: projects.view
Returns the deal linked to this project via the deal_project_links table.
Response: { deal_link } where deal_link is null if no deal is linked, or includes deal_title and deal_status from the joined deal record.