Retainers, client records, delivery and catalogue
Retainers, a client's timeline, tags and custom fields, milestones, subtasks, labels, task dependencies, calendar event writes, services and packages, and document folders: the routes, their scopes, the rules a schema does not show, and the MCP tool for each.
These routes let an integration, an MCP client and Rokn's in-app assistant do the same things: every MCP tool calls exactly one REST route, under the same scope check and the same member permissions. Every path below is under /api/v1/workspaces/{slug} and follows the usual rules: bearer credential, the workspace in the path, problem-details errors. The API reference lists every body and response; this page gives the rules a schema does not show, and names the MCP tool for each route.
A record that belongs to another workspace is always 404 not_found, and a currency other than the workspace's is 409 currency_mismatch.
Replace-sets and deletes
Three writes REPLACE a set instead of merging into it. Send everything you want to keep.
PATCH …/invoices/{id}(invoices:write, toolupdate_invoice) edits a draft invoice. WhenlineItemsis present it is the whole list: a line with itsidis updated, a line without one is added, and any existing line you leave out is deleted. Totals are recomputed from the result. A sent invoice is409 conflict.PUT …/tasks/{id}/labels(tasks:write, toolset_task_labels) sets a task's labels to exactlylabelIds(at most 20;[]clears them). Every label you want to keep must be in the list.PATCH …/packages/{id}(services:write, toolupdate_package):items, when sent, replaces the package's lines. Leave it out to keep them.
Deletes exist only for milestones, deliverables, subtasks, timeline entries and calendar events. There is no delete for services, packages, tags or custom-field definitions: archive a service or a package with active: false.
Retainers
A retainer is an allowance a client buys each period (or once). Creating one opens its first period in the same step, and every read carries the open period and the remaining balance.
GET …/retainers(invoices:read, toollist_retainers): newest first, up to 200, not paginated. Filter bystatus(activeorinactive) orclient_id.GET …/retainers/{id}(invoices:read, toolget_retainer): one retainer with its open period and balance.POST …/retainers(invoices:write, toolcreate_retainer):clientId,name,amountCents,currency(the workspace's),cadence(none,monthlyorquarterly) andstartsOn; optionallycarryover(expireorrollover),rolloverCapCentsandendsOn. A one-off block (cadence: none) requiresendsOn, the end of its single period. Dates are dates: the time of day is dropped.PATCH …/retainers/{id}(invoices:write, toolupdate_retainer): only the fields present are written, andnullclears the cap or the end date. A new fee applies from the NEXT period; the open period keeps the allowance it opened with. The currency cannot change.POST …/retainers/{id}/deactivate(invoices:write, tooldeactivate_retainer) switches a retainer off and keeps its periods and invoices. It is idempotent: deactivating an inactive retainer answers 200.
None of these five operations closes a period.
A client's timeline
GET …/clients/{id}/timeline(communications:read, toollist_client_timeline): notes, calls, meetings, emails and system events, pinned first and then newest first. It is keyset-paginated withlimitandcursor.POST …/clients/{id}/timeline(communications:write, tooladd_timeline_entry): logs anote,call,meetingoremailwithcontent(at most 10,000 characters).subjectdefaults to the content's first line,occurredAtback-dates the entry andprojectIdlinks a project of the workspace.systemis never accepted.PATCH …/timeline/{id}andDELETE …/timeline/{id}(communications:write, toolsupdate_timeline_entryanddelete_timeline_entry): change the type, subject, content (nullclears it) orpinnedflag, or delete the entry and its attachments for good.
A timeline entry of type email only records a conversation; it never sends mail. An entry is authored by a person, so an API key that tries to log one is refused with 403: use an OAuth token. A member edits or deletes only their own entries and owner and admin any; a colleague's entry and a system entry are 404.
Tags and custom fields
GET …/tags(clients:read, toollist_tags) andPOST …/tags(clients:write, toolcreate_tag): a name (trimmed, at most 120 characters) and an optional#rrggbbcolour. A name already used in the workspace is409 tag_name_taken.POST …/clients/{id}/tags(clients:write, tooltag_client) takes atagIdand is idempotent: a client that already carries the tag answers 200 with the same tag.DELETE …/clients/{id}/tags/{tagId}(clients:write, tooluntag_client) detaches the tag and keeps it. A client that does not carry the tag is404.GET …/custom-fields(clients:read, toollist_custom_fields) lists the client field definitions (type, options, whether required) in display order.PUT …/clients/{id}/custom-fields/{fieldId}(clients:write, toolset_custom_field_value) sets, replaces or, withnull, clears one value. Adatefield takesYYYY-MM-DD, aselectone of its options and aurlan http(s) address. A value the field's type does not accept is422and nothing is written.
Tags and custom-field definitions can be listed and created over the API, never deleted.
Milestones, deliverables, subtasks, labels and dependencies
GET …/projects/{id}/milestones(projects:read, toollist_milestones) returns both lists of the project, in order. Deliverables belong to the project, not to a milestone.POST …/projects/{id}/milestonesandPATCH/DELETE …/milestones/{id}(projects:write, toolscreate_milestone,update_milestone,delete_milestone):nameand an optionaldueDate;completed: truecompletes a milestone andfalsereopens it;dueDate: nullclears the date.POST …/projects/{id}/deliverablesandPATCH/DELETE …/deliverables/{id}(projects:write, toolscreate_deliverable,update_deliverable,delete_deliverable):nameandcompleted.POST …/tasks/{id}/subtasksandPATCH/DELETE …/subtasks/{id}(tasks:write, toolscreate_subtask,update_subtask,delete_subtask):titleandcompleted; a new subtask goes after the task's last one.GET …/labels(tasks:read, toollist_labels) andPOST …/labels(tasks:write, toolcreate_label): label names are not unique, so keep theidyou need forPUT …/tasks/{id}/labels.POST …/tasks/{id}/dependencies(tasks:write, tooladd_task_dependency) marks the task as blocked byblockingTaskId, with an optionalnote.POST …/dependencies/{id}/resolve(tasks:write, toolresolve_task_dependency) clears the blocker once the blocking work is done; it stays as history withresolvedAt.
Deleting a milestone, deliverable or subtask that is already gone is 404. Adding a dependency has three refusals, all 409: dependency_self (a task cannot block itself), dependency_cycle (the new dependency would make tasks wait on each other, directly or through others) and dependency_chain_too_long (the check would walk past 500 tasks and fails closed). Adding the same unresolved pair again returns the existing dependency; resolving a resolved one returns it unchanged. A task of another workspace on either side is 404, before any 409.
PUT /api/v1/workspaces/acme/tasks/tsk_123/labels
Authorization: Bearer <token>
Content-Type: application/json
{ "labelIds": ["lbl_bug", "lbl_urgent"] }Calendar events
POST …/calendar-events, PATCH …/calendar-events/{id} and DELETE …/calendar-events/{id} (calendar:write, tools create_calendar_event, update_calendar_event, delete_calendar_event) write stored events. Reading is unchanged; see Calendar, notifications and devices.
- Create takes a
titleandstartTime, atypeofmeeting(the default) orother, and optionallyendTime,allDay,location,description,clientIdandprojectId. An event for a client is also logged on that client's timeline. - With a member's token the event is pushed to a connected Google or Microsoft calendar after it is saved:
pushTonames one of the member's own connections,nonepushes nowhere and leaving it out uses their default connection. Only the title, time and place are pushed: no attendees, and nothing is sent to anyone. ownerUserIdassigns the event to another member of the workspace. Owner, admin and API keys may set it; another workspace's user is404.- An API key is no person, so it must name
ownerUserIdwhen it creates an event, and it never pushes to a connected calendar. Without it the request is422 owner_requiredatownerUserId. - An event imported from a connected Google or Microsoft calendar (
type: "external") belongs to the provider: updating or deleting it is409 event_not_editable. Change it in the provider; a generated item (a task, invoice, milestone or renewal) is changed through its own resource.
POST …/allocations (capacity:write, tool upsert_allocation) replaces the planned minutes for one member, project and week; plannedMinutes: 0 deletes the row. It needs owner or admin, or a credential holding an unrestricted capacity:write.
POST /api/v1/workspaces/acme/calendar-events
Authorization: Bearer <api key>
Content-Type: application/json
{
"title": "Kick-off call",
"startTime": "2026-10-12T09:00:00+02:00",
"endTime": "2026-10-12T09:30:00+02:00",
"clientId": "cli_123",
"ownerUserId": "usr_456"
}Services and packages
POST …/services(services:write, toolcreate_service):name,pricingType(hourlyorfixed) andrateCentsin the workspace currency; optionallydescription,categoryIdandactive.PATCH …/services/{id}(services:write, toolupdate_service):nullclearsdescriptionorcategoryId. ChangingrateCentsorpricingTypecloses the current rate in the history and opens a new one; invoices already issued keep the rate they were issued at.active: falsearchives the service andtruerestores it.POST …/packages(services:write, toolcreate_package) bundles services at one price:name,priceCentsanditems, one{ serviceId, quantity }per service (at least one, each service once). A service of another workspace is404and nothing is created.PATCH …/packages/{id}(services:write, toolupdate_package):nullclearsdescription,active: falsearchives it, anditemsreplaces the package's lines.
Listing services (GET …/services, tool list_services) is unchanged. Neither a service nor a package can be deleted; archive it instead.
Document folders
Folders organise the Documents area. Listing documents and folders (GET …/documents, tool list_documents: type=folder for folders only, parent_id for a folder's contents, root=true for the top level) is unchanged.
POST …/documents/folders(documents:write, toolcreate_folder) creates a folder from aname(at most 255 characters) and, optionally,parentId(the folder to create it in; leave it out for the top level) andprojectId. The folder goes on its parent's storage drive, or on the workspace's default drive at the top level. AparentIdthat is not a folder is404; a parent that belongs to another project thanprojectIdis409 folder_project_mismatch.POST …/documents/{id}/move(documents:write, toolmove_document) moves a document, or a folder with everything in it, into the folderparentId, or to the top level withparentId: null. A folder moved into itself or into one of its own subfolders is409 folder_cycle, and a target folder on another storage drive is409 storage_drive_mismatch; nothing moves. A contract's document moves as it does on the Documents screen; in a workspace without contract signing it is403 feature_locked.
Both routes run the same checks as the Documents screen and are open to the members who may edit documents (owner, admin and member); an accountant or a viewer is 403. A folder or project of another workspace is 404. Uploading a file stays a screen action. Rokn's in-app assistant can also save a file attached to a chat into Documents; that is not an API route.
POST /api/v1/workspaces/acme/documents/folders
Authorization: Bearer <token>
Content-Type: application/json
{ "name": "Contracts" }
# 201 → { "id": "doc_folder_1", "type": "folder", "parentId": null, … }
POST /api/v1/workspaces/acme/documents/doc_123/move
Authorization: Bearer <token>
Content-Type: application/json
{ "parentId": "doc_folder_1" }