Browse documentation DocumentationLet’s talk
Documentation / Build with Rokn

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, tool update_invoice) edits a draft invoice. When lineItems is present it is the whole list: a line with its id is 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 is 409 conflict.
  • PUT …/tasks/{id}/labels (tasks:write, tool set_task_labels) sets a task's labels to exactly labelIds (at most 20; [] clears them). Every label you want to keep must be in the list.
  • PATCH …/packages/{id} (services:write, tool update_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, tool list_retainers): newest first, up to 200, not paginated. Filter by status (active or inactive) or client_id.
  • GET …/retainers/{id} (invoices:read, tool get_retainer): one retainer with its open period and balance.
  • POST …/retainers (invoices:write, tool create_retainer): clientId, name, amountCents, currency (the workspace's), cadence (none, monthly or quarterly) and startsOn; optionally carryover (expire or rollover), rolloverCapCents and endsOn. A one-off block (cadence: none) requires endsOn, the end of its single period. Dates are dates: the time of day is dropped.
  • PATCH …/retainers/{id} (invoices:write, tool update_retainer): only the fields present are written, and null clears 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, tool deactivate_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, tool list_client_timeline): notes, calls, meetings, emails and system events, pinned first and then newest first. It is keyset-paginated with limit and cursor.
  • POST …/clients/{id}/timeline (communications:write, tool add_timeline_entry): logs a note, call, meeting or email with content (at most 10,000 characters). subject defaults to the content's first line, occurredAt back-dates the entry and projectId links a project of the workspace. system is never accepted.
  • PATCH …/timeline/{id} and DELETE …/timeline/{id} (communications:write, tools update_timeline_entry and delete_timeline_entry): change the type, subject, content (null clears it) or pinned flag, 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, tool list_tags) and POST …/tags (clients:write, tool create_tag): a name (trimmed, at most 120 characters) and an optional #rrggbb colour. A name already used in the workspace is 409 tag_name_taken.
  • POST …/clients/{id}/tags (clients:write, tool tag_client) takes a tagId and is idempotent: a client that already carries the tag answers 200 with the same tag.
  • DELETE …/clients/{id}/tags/{tagId} (clients:write, tool untag_client) detaches the tag and keeps it. A client that does not carry the tag is 404.
  • GET …/custom-fields (clients:read, tool list_custom_fields) lists the client field definitions (type, options, whether required) in display order.
  • PUT …/clients/{id}/custom-fields/{fieldId} (clients:write, tool set_custom_field_value) sets, replaces or, with null, clears one value. A date field takes YYYY-MM-DD, a select one of its options and a url an http(s) address. A value the field's type does not accept is 422 and 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, tool list_milestones) returns both lists of the project, in order. Deliverables belong to the project, not to a milestone.
  • POST …/projects/{id}/milestones and PATCH/DELETE …/milestones/{id} (projects:write, tools create_milestone, update_milestone, delete_milestone): name and an optional dueDate; completed: true completes a milestone and false reopens it; dueDate: null clears the date.
  • POST …/projects/{id}/deliverables and PATCH/DELETE …/deliverables/{id} (projects:write, tools create_deliverable, update_deliverable, delete_deliverable): name and completed.
  • POST …/tasks/{id}/subtasks and PATCH/DELETE …/subtasks/{id} (tasks:write, tools create_subtask, update_subtask, delete_subtask): title and completed; a new subtask goes after the task's last one.
  • GET …/labels (tasks:read, tool list_labels) and POST …/labels (tasks:write, tool create_label): label names are not unique, so keep the id you need for PUT …/tasks/{id}/labels.
  • POST …/tasks/{id}/dependencies (tasks:write, tool add_task_dependency) marks the task as blocked by blockingTaskId, with an optional note. POST …/dependencies/{id}/resolve (tasks:write, tool resolve_task_dependency) clears the blocker once the blocking work is done; it stays as history with resolvedAt.

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.

HTTP · replace a task's labels
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 title and startTime, a type of meeting (the default) or other, and optionally endTime, allDay, location, description, clientId and projectId. 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: pushTo names one of the member's own connections, none pushes 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.
  • ownerUserId assigns the event to another member of the workspace. Owner, admin and API keys may set it; another workspace's user is 404.
  • An API key is no person, so it must name ownerUserId when it creates an event, and it never pushes to a connected calendar. Without it the request is 422 owner_required at ownerUserId.
  • An event imported from a connected Google or Microsoft calendar (type: "external") belongs to the provider: updating or deleting it is 409 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.

HTTP · an API key creates an event
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, tool create_service): name, pricingType (hourly or fixed) and rateCents in the workspace currency; optionally description, categoryId and active.
  • PATCH …/services/{id} (services:write, tool update_service): null clears description or categoryId. Changing rateCents or pricingType closes the current rate in the history and opens a new one; invoices already issued keep the rate they were issued at. active: false archives the service and true restores it.
  • POST …/packages (services:write, tool create_package) bundles services at one price: name, priceCents and items, one { serviceId, quantity } per service (at least one, each service once). A service of another workspace is 404 and nothing is created.
  • PATCH …/packages/{id} (services:write, tool update_package): null clears description, active: false archives it, and items replaces 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, tool create_folder) creates a folder from a name (at most 255 characters) and, optionally, parentId (the folder to create it in; leave it out for the top level) and projectId. The folder goes on its parent's storage drive, or on the workspace's default drive at the top level. A parentId that is not a folder is 404; a parent that belongs to another project than projectId is 409 folder_project_mismatch.
  • POST …/documents/{id}/move (documents:write, tool move_document) moves a document, or a folder with everything in it, into the folder parentId, or to the top level with parentId: null. A folder moved into itself or into one of its own subfolders is 409 folder_cycle, and a target folder on another storage drive is 409 storage_drive_mismatch; nothing moves. A contract's document moves as it does on the Documents screen; in a workspace without contract signing it is 403 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.

HTTP · create a folder, then move a document into it
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" }