Skip to main content

Projects API

Project Endpoints

MethodEndpointDescription
GET/projectsList projects (query: statusId, ownerId, search, page, limit)
GET/projects/:idFull project with phases, tasks, members
POST/projectsCreate project
POST/projects/from-opportunityCreate from opportunity + template
PUT/projects/:idUpdate project
DELETE/projects/:idDelete project
GET/projects/:id/kanbanKanban view
GET/projects/:id/ganttGantt timeline data

Team & Members

MethodEndpointDescription
GET/projects/:id/membersList members
POST/projects/:id/membersAdd member
DELETE/projects/:id/members/:memberIdRemove member

Tasks

MethodEndpointDescription
POST/projects/:id/tasksCreate task
PUT/projects/:id/tasks/:taskIdUpdate task
DELETE/projects/:id/tasks/:taskIdDelete task

Dependencies

MethodEndpointDescription
GET/projects/:id/tasks/:taskId/dependenciesGet dependencies
POST/projects/:id/tasks/:taskId/dependenciesAdd dependency
DELETE/projects/:id/tasks/:taskId/dependencies/:depIdRemove

Time Tracking

MethodEndpointDescription
POST/projects/:id/tasks/:taskId/timeLog time entry (requires project_time_entries.create)
GET/projects/:id/tasks/:taskId/time-entriesList entries for a task (for edit/delete UI)
GET/projects/:id/time-entriesList all entries for a project (used by the time report drill-down)
GET/projects/:id/time-reportTime report grouped by user (with per-task breakdown)
GET/projects/:id/time-report/by-taskTime report grouped by task
PUT/projects/time-entries/:entryIdEdit a logged entry (requires project_time_entries.edit)
DELETE/projects/time-entries/:entryIdDelete a logged entry (requires project_time_entries.delete)

Active Timer (one per user)

Server-side stopwatch so a started timer survives page refresh / device switch. A user can only have one active timer at a time.

MethodEndpointDescription
POST/projects/:id/tasks/:taskId/timer/startStart a timer on a task (errors if another timer is active)
POST/projects/timer/stopStop the active timer and log the elapsed time (body: { minutes?, description?, isBillable? }; minutes overrides elapsed if supplied)
GET/projects/timer/activeReturn the current user's active timer or null

Calendar Tasks

CRM tasks (the standalone /tasks module) can be linked to a project — and optionally to a specific project task — via project_id + project_task_id columns. Once linked, they sync to Google/Outlook calendar via the existing calendar-sync wiring.

MethodEndpointDescription
GET/projects/:id/calendar?start=&end=Unified calendar feed for a project: project tasks with due dates + CRM tasks linked to the project + CRM tasks assigned to any project member
GET/projects/:id/tasks/:taskId/calendar-tasksList CRM tasks linked to a specific project task (used by the project task detail panel)

Each event has sourceType: 'project_task' \| 'linked_task' \| 'member_task' so the UI can color-code distinctly.

Client Portal

The portal is gated by email OTP. Token URLs alone are not enough to log in — the client must enter the email associated with the token and verify a 6-digit code sent to that email. Sessions last 24 hours and live in an httpOnly cookie (portal_<tenantSlug>).

Token Management (internal team, requires JWT)

MethodEndpointDescription
GET/projects/:id/portal-tokensList active portal tokens for a project
POST/projects/:id/portal-tokenCreate a portal token (body: { email, label?, expiresAt? } — email required)
DELETE/projects/portal-tokens/:tokenIdRevoke a portal token

Client Messages (internal team side)

MethodEndpointDescription
GET/projects/:id/client-messagesList the project-level message thread
POST/projects/:id/client-messagesSend a message to the client (sender_type = 'team')
GET/projects/:id/client-messages/unread-countCount of client-sent messages not yet read by an internal user

Public Portal Endpoints (no JWT — OTP-gated)

Public step (no auth, no session):

MethodEndpointDescription
GET/portal/:tenantSlug/:token/infoMinimal info for the verify screen (project name, masked email hint)
POST/portal/:tenantSlug/:token/request-otpSend a 6-digit code to the authorized email. 10-min TTL, 5 attempts max, 30s cooldown
POST/portal/:tenantSlug/:token/verify-otpVerify code → sets portal_<tenantSlug> cookie + returns sessionToken
POST/portal/:tenantSlug/:token/logoutClear session

Authenticated portal endpoints (require the cookie or Authorization: Bearer <sessionToken>):

MethodEndpointDescription
GET/portal/:tenantSlug/:token/sessionReturn the resolved session context
GET/portal/:tenantSlug/:token/projectProject overview (only client-visible items counted)
GET/portal/:tenantSlug/:token/tasksTasks marked client_visible = true
GET/portal/:tenantSlug/:token/milestonesMilestones marked client_visible = true
POST/portal/:tenantSlug/:token/milestones/:id/acceptAccept a milestone (records client_accepted_at + email)
GET/portal/:tenantSlug/:token/tasks/:taskId/commentsComments on a client-visible task
POST/portal/:tenantSlug/:token/tasks/:taskId/commentsPost a comment as the client
GET/portal/:tenantSlug/:token/messagesProject-level message thread
POST/portal/:tenantSlug/:token/messagesSend a message to the team

Client Visibility

By default everything is internal. Toggle client_visible = true on individual project tasks and milestones to surface them on the portal. Task comments authored from the portal carry is_from_client = true + the client's email.

Approvals

MethodEndpointDescription
POST/projects/:id/request-approvalTrigger the shared Approval Engine for (projects, project_start)
GET/approvals/rules/check?entityType=projects&triggerEvent=project_startNon-admin check — returns { configured: boolean } so detail pages can hide the Request Approval button when no rule is configured

Admin

MethodEndpointDescription
POST/PATCH/DELETE/projects/admin/project-statuses/:idManage statuses
POST/PATCH/DELETE/projects/admin/task-statuses/:idManage task statuses
POST/PATCH/DELETE/projects/admin/templates/:idManage templates
PUT/projects/admin/templates/:id/structureUpdate template phases + tasks

Health Statuses

on_track, at_risk, off_track