Work: projects, tasks, hours
What the agency actually does, and what it bills: projects with budgets, task boards and checklists, the timer and the timesheet, and every contact moment on one timeline.
99 endpoints across 4 groups. A permission written module.resource.action:own is scoped: :own covers rows that are yours, :any covers everyone’s, and :any satisfies a check for :own.
projects
Section titled “projects”| Endpoint | Permission |
|---|---|
GET /projects | projects.project.read |
POST /projects | projects.project.write |
GET /projects/{project_id} | projects.project.read |
PATCH /projects/{project_id} | projects.project.write |
DELETE /projects/{project_id} | projects.project.delete |
GET /projects/dashboard-budgets | projects.project.read |
Parameters and schemas per endpoint
GET /api/v1/projects
Section titled “GET /api/v1/projects”List Projects
Permission: projects.project.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
limit | integer | (default 50) |
offset | integer | (default 0) |
company_id | string | |
status | string | Lifecycle status; comma-separate for several (‘active,on_hold’). Absent means every status, the archived ones included — the screen picks its own default, this endpoint does not. |
q | string | |
unnamed | boolean | Only projects nobody named (create-then-edit rows never finished), or only named ones. Omitted returns both. |
mine | boolean | Only projects I’m assigned to (primary or not) (default false) |
sort | string | name | status | start_date | end_date | budget_hours | …, ’-’ desc |
hours | boolean | Include the budget burn-down; costs one grouped query (default false) |
count | boolean | Compute total; set false for name-only lookups (default true) |
Returns: 200 · Page_ProjectRead_
POST /api/v1/projects
Section titled “POST /api/v1/projects”Create Project
Permission: projects.project.write
Request body: ProjectCreate
Returns: 201 · ProjectRead
GET /api/v1/projects/{project_id}
Section titled “GET /api/v1/projects/{project_id}”Get Project
Permission: projects.project.read
Path parameters: project_id
Query parameters:
| Name | Type | Meaning |
|---|---|---|
hours | boolean | Include the budget burn-down for the current period; one extra query (default false) |
Returns: 200 · ProjectRead
PATCH /api/v1/projects/{project_id}
Section titled “PATCH /api/v1/projects/{project_id}”Update Project
Permission: projects.project.write
Path parameters: project_id
Request body: ProjectUpdate
Returns: 200 · ProjectRead
DELETE /api/v1/projects/{project_id}
Section titled “DELETE /api/v1/projects/{project_id}”Delete Project
Permission: projects.project.delete
Path parameters: project_id
Returns: 204
GET /api/v1/projects/dashboard-budgets
Section titled “GET /api/v1/projects/dashboard-budgets”Dashboard Budgets — The budgeted active projects burning hottest — the My Day tile, already sorted and cut.
Permission: projects.project.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
limit | integer | (default 4) |
Returns: 200 · DashboardBudgetProject[]
| Endpoint | Permission |
|---|---|
GET /tasks | tasks.task.read |
POST /tasks | tasks.task.create |
GET /tasks/{task_id} | tasks.task.read |
PATCH /tasks/{task_id} | tasks.task.write |
DELETE /tasks/{task_id} | tasks.task.delete |
GET /tasks/{task_id}/ai-status | tasks.task.read |
POST /tasks/{task_id}/checklists | tasks.task.write |
PATCH /tasks/{task_id}/checklists/{checklist_id} | tasks.task.write |
DELETE /tasks/{task_id}/checklists/{checklist_id} | tasks.task.write |
POST /tasks/{task_id}/checklists/{checklist_id}/duplicate | tasks.task.write |
POST /tasks/{task_id}/checklists/{checklist_id}/items | tasks.task.write |
PATCH /tasks/{task_id}/checklists/{checklist_id}/items/{item_id} | tasks.task.write |
DELETE /tasks/{task_id}/checklists/{checklist_id}/items/{item_id} | tasks.task.write |
POST /tasks/{task_id}/checklists/{checklist_id}/items/order | tasks.task.write |
POST /tasks/{task_id}/checklists/order | tasks.task.write |
POST /tasks/{task_id}/comments | tasks.comment.write |
PATCH /tasks/{task_id}/comments/{comment_id} | tasks.comment.write |
DELETE /tasks/{task_id}/comments/{comment_id} | tasks.comment.write |
PUT /tasks/{task_id}/labels | tasks.task.write |
POST /tasks/{task_id}/links | tasks.task.write |
DELETE /tasks/{task_id}/links/{link_id} | tasks.task.write |
GET /tasks/checklist-templates | tasks.task.write |
POST /tasks/checklist-templates | tasks.checklist_template.write |
PATCH /tasks/checklist-templates/{template_id} | tasks.checklist_template.write |
DELETE /tasks/checklist-templates/{template_id} | tasks.checklist_template.write |
GET /tasks/dashboard-groups | tasks.task.read |
GET /tasks/dashboard-mine | tasks.task.read |
GET /tasks/labels | tasks.task.read |
POST /tasks/labels | tasks.label.write |
PATCH /tasks/labels/{label_id} | tasks.label.write |
DELETE /tasks/labels/{label_id} | tasks.label.write |
GET /tasks/mine | tasks.task.read |
POST /tasks/recurrence/preview | tasks.task.write |
GET /tasks/schedules | tasks.schedule.read |
POST /tasks/schedules | tasks.schedule.write |
GET /tasks/schedules/{schedule_id} | tasks.schedule.read |
PATCH /tasks/schedules/{schedule_id} | tasks.schedule.write |
DELETE /tasks/schedules/{schedule_id} | tasks.schedule.write |
POST /tasks/schedules/{schedule_id}/log-time | time.entry.write |
GET /tasks/statuses | tasks.task.read |
POST /tasks/statuses | tasks.status.write |
PATCH /tasks/statuses/{status_id} | tasks.status.write |
DELETE /tasks/statuses/{status_id} | tasks.status.write |
GET /tasks/templates | tasks.template.apply |
POST /tasks/templates | tasks.template.write |
GET /tasks/templates/{template_id} | tasks.template.apply |
PATCH /tasks/templates/{template_id} | tasks.template.write |
DELETE /tasks/templates/{template_id} | tasks.template.write |
POST /tasks/templates/{template_id}/apply | tasks.template.apply |
Parameters and schemas per endpoint
GET /api/v1/tasks
Section titled “GET /api/v1/tasks”List Tasks
Permission: tasks.task.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
limit | integer | (default 50) |
offset | integer | (default 0) |
company_id | string | |
project_id | string | |
unlinked | boolean | Only tasks with no client and no project (default false) |
assignee_user_id | string | |
assignee_contact_id | string | |
status | string | A configured status key |
label_id | string | |
due | overdue · today · week | |
due_from | string | Deadline window start (the Agenda feed) |
due_to | string | Deadline window end (inclusive) |
q | string | |
unnamed | boolean | Only tasks nobody named (create-then-edit rows never finished), or only named ones. Omitted returns both. |
sort | string | title | due_date | priority | status | assignee | …, ’-’ desc |
meta | boolean | Include label/checklist/comment aggregates (default true) |
hours | boolean | Include the hour budget’s burn (logged/remaining minutes); costs one grouped query. Omitted for a caller without time.entry.read rather than refused. (default false) |
count | boolean | Compute total; set false for name-only lookups (default true) |
Returns: 200 · Page_TaskListItem_
POST /api/v1/tasks
Section titled “POST /api/v1/tasks”Create Task
Permission: tasks.task.create
Request body: TaskCreate
Returns: 201 · TaskRead
GET /api/v1/tasks/{task_id}
Section titled “GET /api/v1/tasks/{task_id}”Get Task — The full card: labels, checklists, comments and recent activity included.
Permission: tasks.task.read
Path parameters: task_id
Returns: 200 · TaskDetail
PATCH /api/v1/tasks/{task_id}
Section titled “PATCH /api/v1/tasks/{task_id}”Update Task
Permission: tasks.task.write
Path parameters: task_id
Request body: TaskUpdate
Returns: 200 · TaskRead
DELETE /api/v1/tasks/{task_id}
Section titled “DELETE /api/v1/tasks/{task_id}”Delete Task
Permission: tasks.task.delete
Path parameters: task_id
Returns: 204
GET /api/v1/tasks/{task_id}/ai-status
Section titled “GET /api/v1/tasks/{task_id}/ai-status”Get Task Ai Status — Just the “is schakl still filling this in?” flag (#327).
Permission: tasks.task.read
Path parameters: task_id
Returns: 200 · TaskAIStatusRead
POST /api/v1/tasks/{task_id}/checklists
Section titled “POST /api/v1/tasks/{task_id}/checklists”Add Checklist
Permission: tasks.task.write
Path parameters: task_id
Request body: ChecklistCreate
Returns: 201 · ChecklistRead
PATCH /api/v1/tasks/{task_id}/checklists/{checklist_id}
Section titled “PATCH /api/v1/tasks/{task_id}/checklists/{checklist_id}”Update Checklist
Permission: tasks.task.write
Path parameters: task_id, checklist_id
Request body: ChecklistUpdate
Returns: 200 · ChecklistRead
DELETE /api/v1/tasks/{task_id}/checklists/{checklist_id}
Section titled “DELETE /api/v1/tasks/{task_id}/checklists/{checklist_id}”Delete Checklist
Permission: tasks.task.write
Path parameters: task_id, checklist_id
Returns: 204
POST /api/v1/tasks/{task_id}/checklists/{checklist_id}/duplicate
Section titled “POST /api/v1/tasks/{task_id}/checklists/{checklist_id}/duplicate”Duplicate Checklist — Copy a checklist beside its source, items and all — a second run of the same steps.
Permission: tasks.task.write
Path parameters: task_id, checklist_id
Request body: ChecklistDuplicate
Returns: 201 · ChecklistRead
POST /api/v1/tasks/{task_id}/checklists/{checklist_id}/items
Section titled “POST /api/v1/tasks/{task_id}/checklists/{checklist_id}/items”Add Checklist Item
Permission: tasks.task.write
Path parameters: task_id, checklist_id
Request body: ChecklistItemCreate
Returns: 201 · ChecklistItemRead
PATCH /api/v1/tasks/{task_id}/checklists/{checklist_id}/items/{item_id}
Section titled “PATCH /api/v1/tasks/{task_id}/checklists/{checklist_id}/items/{item_id}”Update Checklist Item
Permission: tasks.task.write
Path parameters: task_id, checklist_id, item_id
Request body: ChecklistItemUpdate
Returns: 200 · ChecklistItemRead
DELETE /api/v1/tasks/{task_id}/checklists/{checklist_id}/items/{item_id}
Section titled “DELETE /api/v1/tasks/{task_id}/checklists/{checklist_id}/items/{item_id}”Delete Checklist Item
Permission: tasks.task.write
Path parameters: task_id, checklist_id, item_id
Returns: 204
POST /api/v1/tasks/{task_id}/checklists/{checklist_id}/items/order
Section titled “POST /api/v1/tasks/{task_id}/checklists/{checklist_id}/items/order”Reorder Checklist Items — Set the order of one checklist’s items in one call.
Permission: tasks.task.write
Path parameters: task_id, checklist_id
Request body: ChecklistItemOrder
Returns: 200 · ChecklistOrderRead
POST /api/v1/tasks/{task_id}/checklists/order
Section titled “POST /api/v1/tasks/{task_id}/checklists/order”Reorder Checklists — Set the order of a task’s checklists in one call (ChecklistOrder for the contract).
Permission: tasks.task.write
Path parameters: task_id
Request body: ChecklistOrder
Returns: 200 · ChecklistOrderRead
POST /api/v1/tasks/{task_id}/comments
Section titled “POST /api/v1/tasks/{task_id}/comments”Add Comment
Permission: tasks.comment.write
Path parameters: task_id
Request body: CommentCreate
Returns: 201 · CommentRead
PATCH /api/v1/tasks/{task_id}/comments/{comment_id}
Section titled “PATCH /api/v1/tasks/{task_id}/comments/{comment_id}”Update Comment
Permission: tasks.comment.write
Path parameters: task_id, comment_id
Request body: CommentUpdate
Returns: 200 · CommentRead
DELETE /api/v1/tasks/{task_id}/comments/{comment_id}
Section titled “DELETE /api/v1/tasks/{task_id}/comments/{comment_id}”Delete Comment
Permission: tasks.comment.write
Path parameters: task_id, comment_id
Returns: 204
PUT /api/v1/tasks/{task_id}/labels
Section titled “PUT /api/v1/tasks/{task_id}/labels”Set Task Labels
Permission: tasks.task.write
Path parameters: task_id
Request body: TaskLabelsSet
Returns: 200 · LabelRead[]
POST /api/v1/tasks/{task_id}/links
Section titled “POST /api/v1/tasks/{task_id}/links”Add Link
Permission: tasks.task.write
Path parameters: task_id
Request body: app__modules__tasks__schemas__LinkCreate
Returns: 201 · app__modules__tasks__schemas__LinkRead
DELETE /api/v1/tasks/{task_id}/links/{link_id}
Section titled “DELETE /api/v1/tasks/{task_id}/links/{link_id}”Delete Link
Permission: tasks.task.write
Path parameters: task_id, link_id
Returns: 204
GET /api/v1/tasks/checklist-templates
Section titled “GET /api/v1/tasks/checklist-templates”List Checklist Templates
Permission: tasks.task.write
Returns: 200 · ChecklistTemplateRead[]
POST /api/v1/tasks/checklist-templates
Section titled “POST /api/v1/tasks/checklist-templates”Create Checklist Template
Permission: tasks.checklist_template.write
Request body: ChecklistTemplateCreate
Returns: 201 · ChecklistTemplateRead
PATCH /api/v1/tasks/checklist-templates/{template_id}
Section titled “PATCH /api/v1/tasks/checklist-templates/{template_id}”Update Checklist Template
Permission: tasks.checklist_template.write
Path parameters: template_id
Request body: ChecklistTemplateUpdate
Returns: 200 · ChecklistTemplateRead
DELETE /api/v1/tasks/checklist-templates/{template_id}
Section titled “DELETE /api/v1/tasks/checklist-templates/{template_id}”Delete Checklist Template
Permission: tasks.checklist_template.write
Path parameters: template_id
Returns: 204
GET /api/v1/tasks/dashboard-groups
Section titled “GET /api/v1/tasks/dashboard-groups”Dashboard Groups — Open-task counts grouped by project, then company, in one compact query.
Permission: tasks.task.read
Returns: 200 · DashboardTaskGroup[]
GET /api/v1/tasks/dashboard-mine
Section titled “GET /api/v1/tasks/dashboard-mine”Dashboard Mine — Compact personal task list for the dashboard tile.
Permission: tasks.task.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
limit | integer | (default 20) |
Returns: 200 · DashboardTaskItem[]
GET /api/v1/tasks/labels
Section titled “GET /api/v1/tasks/labels”List Labels
Permission: tasks.task.read
Returns: 200 · LabelRead[]
POST /api/v1/tasks/labels
Section titled “POST /api/v1/tasks/labels”Create Label
Permission: tasks.label.write
Request body: LabelCreate
Returns: 201 · LabelRead
PATCH /api/v1/tasks/labels/{label_id}
Section titled “PATCH /api/v1/tasks/labels/{label_id}”Update Label
Permission: tasks.label.write
Path parameters: label_id
Request body: LabelUpdate
Returns: 200 · LabelRead
DELETE /api/v1/tasks/labels/{label_id}
Section titled “DELETE /api/v1/tasks/labels/{label_id}”Delete Label
Permission: tasks.label.write
Path parameters: label_id
Returns: 204
GET /api/v1/tasks/mine
Section titled “GET /api/v1/tasks/mine”My Open Tasks — Open/in-progress tasks assigned to the current user (My Day).
Permission: tasks.task.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
limit | integer | (default 20) |
Returns: 200 · TaskListItem[]
POST /api/v1/tasks/recurrence/preview
Section titled “POST /api/v1/tasks/recurrence/preview”Preview Recurrence — What the rule being composed resolves to — “Volgende taak: za 13 sep 2026”.
Permission: tasks.task.write
Request body: RecurrencePreview
Returns: 200 · RecurrencePreviewRead
GET /api/v1/tasks/schedules
Section titled “GET /api/v1/tasks/schedules”List Schedules
Permission: tasks.schedule.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
date_from | string | |
date_to | string | |
user_ids | string[] | |
task_id | string |
Returns: 200 · ScheduleItem[]
POST /api/v1/tasks/schedules
Section titled “POST /api/v1/tasks/schedules”Create Schedule
Permission: tasks.schedule.write
Request body: ScheduleCreate
Returns: 201 · ScheduleRead
GET /api/v1/tasks/schedules/{schedule_id}
Section titled “GET /api/v1/tasks/schedules/{schedule_id}”Get Schedule
Permission: tasks.schedule.read
Path parameters: schedule_id
Returns: 200 · ScheduleRead
PATCH /api/v1/tasks/schedules/{schedule_id}
Section titled “PATCH /api/v1/tasks/schedules/{schedule_id}”Update Schedule
Permission: tasks.schedule.write
Path parameters: schedule_id
Request body: ScheduleUpdate
Returns: 200 · ScheduleRead
DELETE /api/v1/tasks/schedules/{schedule_id}
Section titled “DELETE /api/v1/tasks/schedules/{schedule_id}”Delete Schedule
Permission: tasks.schedule.write
Path parameters: schedule_id
Returns: 204
POST /api/v1/tasks/schedules/{schedule_id}/log-time
Section titled “POST /api/v1/tasks/schedules/{schedule_id}/log-time”Log Schedule Time
Permission: time.entry.write
Path parameters: schedule_id
Request body: ScheduleLogTime
Returns: 200 · ScheduleRead
GET /api/v1/tasks/statuses
Section titled “GET /api/v1/tasks/statuses”List Statuses
Permission: tasks.task.read
Returns: 200 · StatusRead[]
POST /api/v1/tasks/statuses
Section titled “POST /api/v1/tasks/statuses”Create Status
Permission: tasks.status.write
Request body: StatusCreate
Returns: 201 · StatusRead
PATCH /api/v1/tasks/statuses/{status_id}
Section titled “PATCH /api/v1/tasks/statuses/{status_id}”Update Status
Permission: tasks.status.write
Path parameters: status_id
Request body: StatusUpdate
Returns: 200 · StatusRead
DELETE /api/v1/tasks/statuses/{status_id}
Section titled “DELETE /api/v1/tasks/statuses/{status_id}”Delete Status
Permission: tasks.status.write
Path parameters: status_id
Returns: 204
GET /api/v1/tasks/templates
Section titled “GET /api/v1/tasks/templates”List Templates
Permission: tasks.template.apply
Returns: 200 · app__modules__tasks__schemas__TemplateRead[]
POST /api/v1/tasks/templates
Section titled “POST /api/v1/tasks/templates”Create Template
Permission: tasks.template.write
Request body: app__modules__tasks__schemas__TemplateCreate
Returns: 201 · app__modules__tasks__schemas__TemplateRead
GET /api/v1/tasks/templates/{template_id}
Section titled “GET /api/v1/tasks/templates/{template_id}”Get Template
Permission: tasks.template.apply
Path parameters: template_id
Returns: 200 · app__modules__tasks__schemas__TemplateRead
PATCH /api/v1/tasks/templates/{template_id}
Section titled “PATCH /api/v1/tasks/templates/{template_id}”Update Template
Permission: tasks.template.write
Path parameters: template_id
Request body: app__modules__tasks__schemas__TemplateUpdate
Returns: 200 · app__modules__tasks__schemas__TemplateRead
DELETE /api/v1/tasks/templates/{template_id}
Section titled “DELETE /api/v1/tasks/templates/{template_id}”Delete Template
Permission: tasks.template.write
Path parameters: template_id
Returns: 204
POST /api/v1/tasks/templates/{template_id}/apply
Section titled “POST /api/v1/tasks/templates/{template_id}/apply”Apply Template
Permission: tasks.template.apply
Path parameters: template_id
Request body: TemplateApply
Returns: 201 · TaskRead[]
| Endpoint | Permission |
|---|---|
GET /time/cost | time.report.read |
GET /time/day | time.entry.read |
PUT /time/drafts/{entry_date} | time.entry.write |
DELETE /time/drafts/{entry_date} | time.entry.write |
GET /time/entries | time.entry.read |
POST /time/entries | time.entry.write |
GET /time/entries/{entry_id} | time.entry.read |
PATCH /time/entries/{entry_id} | time.entry.write |
DELETE /time/entries/{entry_id} | time.entry.write |
POST /time/entries/approve | time.entry.approve |
POST /time/entries/invoice | time.entry.invoice |
GET /time/entry-types | time.entry_type.read |
POST /time/entry-types | time.entry_type.manage |
PATCH /time/entry-types/{type_id} | time.entry_type.manage |
DELETE /time/entry-types/{type_id} | time.entry_type.manage |
GET /time/logged | time.entry.read |
GET /time/report | time.report.read |
GET /time/stats/productivity | time.report.read |
GET /time/stats/revenue | time.report.read |
GET /time/stats/team-summary | time.report.read |
GET /time/summary | time.entry.read |
GET /time/timer | time.entry.read |
POST /time/timer/start | time.entry.write |
POST /time/timer/stop | time.entry.write |
GET /time/timesheet | time.entry.read |
GET /time/workspace | time.entry.read |
Parameters and schemas per endpoint
GET /api/v1/time/cost
Section titled “GET /api/v1/time/cost”Project Cost — A project’s cost from employee rates (#111). The service also demands leave.rate.read:any — the figure is salary-derived.
Permission: time.report.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
project_id | string · required |
Returns: 200 · ProjectCost
GET /api/v1/time/day
Section titled “GET /api/v1/time/day”Day View — One day’s entries (calendar day view) plus total + billable minutes.
Permission: time.entry.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
date | string · required | |
user_id | string |
Returns: 200 · DayView
PUT /api/v1/time/drafts/{entry_date}
Section titled “PUT /api/v1/time/drafts/{entry_date}”Upsert Draft — Upsert the caller’s own draft for a day. Author-only by construction.
Permission: time.entry.write
Path parameters: entry_date
Request body: TimeEntryDraftPayload
Returns: 200 · TimeEntryDraftRead
DELETE /api/v1/time/drafts/{entry_date}
Section titled “DELETE /api/v1/time/drafts/{entry_date}”Delete Draft — Discard the caller’s own draft. Idempotent.
Permission: time.entry.write
Path parameters: entry_date
Returns: 204
GET /api/v1/time/entries
Section titled “GET /api/v1/time/entries”List Entries
Permission: time.entry.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
limit | integer | (default 50) |
offset | integer | (default 0) |
user_id | string | |
company_id | string | |
project_id | string | |
task_id | string | |
date_from | string | |
date_to | string | |
entry_type | string | Filter by entry type key |
running | boolean | Filter running timers in/out; unset = both |
all_users | boolean | The whole team’s entries, not just mine. Free to anyone when the query names a company/project/task — those hours already show as a budget bar; managers only when it doesn’t. (default false) |
sort | string | date | employee | company | project | task | minutes | …, ’-’ desc |
count | boolean | Compute total; set false for lightweight lookups (default true) |
Returns: 200 · Page_TimeEntryRead_
POST /api/v1/time/entries
Section titled “POST /api/v1/time/entries”Create Entry
Permission: time.entry.write
Request body: TimeEntryCreate
Returns: 201 · TimeEntryRead
GET /api/v1/time/entries/{entry_id}
Section titled “GET /api/v1/time/entries/{entry_id}”Get Entry
Permission: time.entry.read
Path parameters: entry_id
Returns: 200 · TimeEntryRead
PATCH /api/v1/time/entries/{entry_id}
Section titled “PATCH /api/v1/time/entries/{entry_id}”Update Entry
Permission: time.entry.write
Path parameters: entry_id
Request body: TimeEntryUpdate
Returns: 200 · TimeEntryRead
DELETE /api/v1/time/entries/{entry_id}
Section titled “DELETE /api/v1/time/entries/{entry_id}”Delete Entry
Permission: time.entry.write
Path parameters: entry_id
Returns: 204
POST /api/v1/time/entries/approve
Section titled “POST /api/v1/time/entries/approve”Approve Entries
Permission: time.entry.approve
Request body: EntryApproval
Returns: 200 · BulkResult
POST /api/v1/time/entries/invoice
Section titled “POST /api/v1/time/entries/invoice”Invoice Entries
Permission: time.entry.invoice
Request body: EntryInvoiced
Returns: 200 · BulkResult
GET /api/v1/time/entry-types
Section titled “GET /api/v1/time/entry-types”List Entry Types
Permission: time.entry_type.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
include_inactive | boolean | (default false) |
Returns: 200 · TimeEntryTypeRead[]
POST /api/v1/time/entry-types
Section titled “POST /api/v1/time/entry-types”Create Entry Type
Permission: time.entry_type.manage
Request body: TimeEntryTypeCreate
Returns: 201 · TimeEntryTypeRead
PATCH /api/v1/time/entry-types/{type_id}
Section titled “PATCH /api/v1/time/entry-types/{type_id}”Update Entry Type
Permission: time.entry_type.manage
Path parameters: type_id
Request body: TimeEntryTypeUpdate
Returns: 200 · TimeEntryTypeRead
DELETE /api/v1/time/entry-types/{type_id}
Section titled “DELETE /api/v1/time/entry-types/{type_id}”Delete Entry Type
Permission: time.entry_type.manage
Path parameters: type_id
Returns: 204
GET /api/v1/time/logged
Section titled “GET /api/v1/time/logged”Logged — Team-wide logged minutes for a company/project/task (budget burn-down).
Permission: time.entry.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
company_id | string | |
project_id | string | |
task_id | string | |
date_from | string | |
date_to | string |
Returns: 200 · LoggedSummary
GET /api/v1/time/report
Section titled “GET /api/v1/time/report”Time Report — Org-wide entries with filter + sign-off totals, for the hours overview.
Permission: time.report.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
limit | integer | (default 200) |
offset | integer | (default 0) |
user_id | string | |
company_id | string | |
project_id | string | |
date_from | string | |
date_to | string | |
billable | boolean | |
approved | boolean | |
invoiced | boolean | |
entry_type | string | Filter by entry type key |
sort | string | date | employee | company | project | task | minutes | …, ’-’ desc |
Returns: 200 · TimeReport
GET /api/v1/time/stats/productivity
Section titled “GET /api/v1/time/stats/productivity”Productivity Stats — Per-employee hours/billable/approved aggregates (managers).
Permission: time.report.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
date_from | string · required | |
date_to | string · required |
Returns: 200 · ProductivityStats
GET /api/v1/time/stats/revenue
Section titled “GET /api/v1/time/stats/revenue”Revenue Stats — Monthly omzet for the selected + previous year and the top clients (managers).
Permission: time.report.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
year | integer · required |
Returns: 200 · RevenueStats
GET /api/v1/time/stats/team-summary
Section titled “GET /api/v1/time/stats/team-summary”Team Time Summary — One-query hours + revenue payload for the manager dashboard tile.
Permission: time.report.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
date_from | string · required | |
date_to | string · required |
Returns: 200 · TeamTimeSummary
GET /api/v1/time/summary
Section titled “GET /api/v1/time/summary”Summary
Permission: time.entry.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
date | string | |
user_id | string |
Returns: 200 · TimeSummary
GET /api/v1/time/timer
Section titled “GET /api/v1/time/timer”Current Timer
Permission: time.entry.read
Returns: 200 · TimeEntryRead
POST /api/v1/time/timer/start
Section titled “POST /api/v1/time/timer/start”Start Timer
Permission: time.entry.write
Request body: TimerStart
Returns: 201 · TimeEntryRead
POST /api/v1/time/timer/stop
Section titled “POST /api/v1/time/timer/stop”Stop Timer
Permission: time.entry.write
Returns: 200 · TimeEntryRead
GET /api/v1/time/timesheet
Section titled “GET /api/v1/time/timesheet”Timesheet
Permission: time.entry.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
week_start | string · required | |
user_id | string |
Returns: 200 · Timesheet
GET /api/v1/time/workspace
Section titled “GET /api/v1/time/workspace”Time Workspace — The interactive Hours screen in one request, reusing its weekly entry scan.
Permission: time.entry.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
week_start | string · required | |
day | string · required |
Returns: 200 · TimeWorkspace
interactions
Section titled “interactions”| Endpoint | Permission |
|---|---|
GET /interactions | interactions.interaction.read |
POST /interactions | interactions.interaction.write |
GET /interactions/{interaction_id} | interactions.interaction.read |
PATCH /interactions/{interaction_id} | interactions.interaction.write |
DELETE /interactions/{interaction_id} | interactions.interaction.delete |
POST /interactions/{interaction_id}/add-to-conversation | interactions.interaction.review |
POST /interactions/{interaction_id}/approve | interactions.interaction.review |
POST /interactions/{interaction_id}/reject | interactions.interaction.review |
POST /interactions/{interaction_id}/remap | interactions.interaction.review |
GET /interactions/{interaction_id}/thread | interactions.interaction.read |
POST /interactions/bulk/approve | interactions.interaction.review |
POST /interactions/bulk/assign | interactions.interaction.review |
POST /interactions/bulk/reject | interactions.interaction.review |
GET /interactions/kinds | interactions.kind.read |
POST /interactions/kinds | interactions.kind.manage |
PATCH /interactions/kinds/{kind_id} | interactions.kind.manage |
DELETE /interactions/kinds/{kind_id} | interactions.kind.manage |
POST /interactions/upload-eml | interactions.interaction.write |
Parameters and schemas per endpoint
GET /api/v1/interactions
Section titled “GET /api/v1/interactions”List Interactions
Permission: interactions.interaction.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
company_id | string | |
project_id | string | |
task_id | string | |
contact_id | string | |
kind | string | |
status | string | |
owner_user_id | string | |
mine | boolean | Only my own rows — the review queue’s filter (default false) |
include | string | Roll-up: ‘tasks’ with project_id also returns the project’s tasks’ rows |
q | string | Free text over subject/snippet/body |
date_from | string | Occurred on/after this org-local day |
date_to | string | Occurred on/before this org-local day |
sort | string | occurred_at | subject | kind | contact | owner, ’-’ desc |
limit | integer | (default 20) |
offset | integer | (default 0) |
count | boolean | Compute the total. False skips a second full pass over the filter. (default true) |
with_body | boolean | Include each row’s full body_text. Off by default — the list draws snippet. (default false) |
Returns: 200 · Page_InteractionRead_
POST /api/v1/interactions
Section titled “POST /api/v1/interactions”Create Interaction
Permission: interactions.interaction.write
Request body: InteractionCreate
Returns: 201 · InteractionRead
GET /api/v1/interactions/{interaction_id}
Section titled “GET /api/v1/interactions/{interaction_id}”Get Interaction
Permission: interactions.interaction.read
Path parameters: interaction_id
Returns: 200 · InteractionRead
PATCH /api/v1/interactions/{interaction_id}
Section titled “PATCH /api/v1/interactions/{interaction_id}”Update Interaction
Permission: interactions.interaction.write
Path parameters: interaction_id
Request body: InteractionUpdate
Returns: 200 · InteractionRead
DELETE /api/v1/interactions/{interaction_id}
Section titled “DELETE /api/v1/interactions/{interaction_id}”Delete Interaction
Permission: interactions.interaction.delete
Path parameters: interaction_id
Returns: 204
POST /api/v1/interactions/{interaction_id}/add-to-conversation
Section titled “POST /api/v1/interactions/{interaction_id}/add-to-conversation”Add Interaction To Conversation — Manually glue this gmail email onto another’s conversation (#272). Gated on .review like every gmail-row mutation — the service enforces strict mailbox ownership on both the row and the target.
Permission: interactions.interaction.review
Path parameters: interaction_id
Request body: InteractionAddToConversation
Returns: 200 · InteractionRead
POST /api/v1/interactions/{interaction_id}/approve
Section titled “POST /api/v1/interactions/{interaction_id}/approve”Approve Interaction
Permission: interactions.interaction.review
Path parameters: interaction_id
Request body: InteractionApprove
Returns: 200 · InteractionRead
POST /api/v1/interactions/{interaction_id}/reject
Section titled “POST /api/v1/interactions/{interaction_id}/reject”Reject Interaction
Permission: interactions.interaction.review
Path parameters: interaction_id
Request body: InteractionReject
Returns: 204
POST /api/v1/interactions/{interaction_id}/remap
Section titled “POST /api/v1/interactions/{interaction_id}/remap”Remap Interaction
Permission: interactions.interaction.review
Path parameters: interaction_id
Request body: InteractionRemap
Returns: 200 · InteractionRead
GET /api/v1/interactions/{interaction_id}/thread
Section titled “GET /api/v1/interactions/{interaction_id}/thread”Get Interaction Thread — The full conversation this interaction belongs to (#272), newest first — what the detail modal expands into. A row not in a conversation is its own one-message thread.
Permission: interactions.interaction.read
Path parameters: interaction_id
Returns: 200 · InteractionRead[]
POST /api/v1/interactions/bulk/approve
Section titled “POST /api/v1/interactions/bulk/approve”Bulk Approve Interactions — Approve a selection, optionally filing all of it in one step.
Permission: interactions.interaction.review
Request body: InteractionBulkApprove
Returns: 200 · InteractionBulkResult
POST /api/v1/interactions/bulk/assign
Section titled “POST /api/v1/interactions/bulk/assign”Bulk Assign Interactions — File a selection without approving it — the batch form of remap, so it re-files logged rows too. An absent link field leaves every row’s own alone.
Permission: interactions.interaction.review
Request body: InteractionBulkAssign
Returns: 200 · InteractionBulkResult
POST /api/v1/interactions/bulk/reject
Section titled “POST /api/v1/interactions/bulk/reject”Bulk Reject Interactions — Reject a selection. Permanent per row: the metadata goes and the message is suppressed, so a re-poll never resurrects it.
Permission: interactions.interaction.review
Request body: InteractionBulkReject
Returns: 200 · InteractionBulkResult
GET /api/v1/interactions/kinds
Section titled “GET /api/v1/interactions/kinds”List Interaction Kinds
Permission: interactions.kind.read
Query parameters:
| Name | Type | Meaning |
|---|---|---|
include_inactive | boolean | (default false) |
Returns: 200 · InteractionKindDefRead[]
POST /api/v1/interactions/kinds
Section titled “POST /api/v1/interactions/kinds”Create Interaction Kind
Permission: interactions.kind.manage
Request body: InteractionKindDefCreate
Returns: 201 · InteractionKindDefRead
PATCH /api/v1/interactions/kinds/{kind_id}
Section titled “PATCH /api/v1/interactions/kinds/{kind_id}”Update Interaction Kind
Permission: interactions.kind.manage
Path parameters: kind_id
Request body: InteractionKindDefUpdate
Returns: 200 · InteractionKindDefRead
DELETE /api/v1/interactions/kinds/{kind_id}
Section titled “DELETE /api/v1/interactions/kinds/{kind_id}”Delete Interaction Kind
Permission: interactions.kind.manage
Path parameters: kind_id
Returns: 204
POST /api/v1/interactions/upload-eml
Section titled “POST /api/v1/interactions/upload-eml”Upload Interaction Eml — Log an exported email as a contactmoment (#262).
Permission: interactions.interaction.write
Request body: Body_upload_interaction_eml_api_v1_interactions_upload_eml_post
Returns: 201 · InteractionEmlUploadRead