Skip to content

GET /trash

What is in the trash

One entry per deletion, newest first - not one per deleted row. A hub with two hundred entries under it went into the trash as one act and comes back as one act, so what is listed is the root of each deletion: the thing somebody deleted. The batch beside it is what took the rest, and restoring the root brings all of it back. The list spans hubs, so it is narrowed to what the caller may see rather than refused: an entry deleted out of a hub the caller has no access to is not in the page. Pages can therefore be shorter than the requested size; walk on until has_more is false.

Query parameters

NameTypeRequiredDescription
cursorstring optional
sizeinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional

Responses

Example bash
curl -X GET "$HUBTASK_URL/trash" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

POST /trash:empty

Remove everything in the trash for good

Irreversible, and it reaches hubs and collections as readily as entries - which is why it needs the owner's right to delete a container rather than the right to write entries. Anything under a legal hold stays, and is counted in the answer rather than failing the call. A large trash takes more than one pass: matched says how many rows this pass considered, and calling again continues where it left off.

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Responses

Example bash
curl -X POST "$HUBTASK_URL/trash:empty" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)"

GET /items

List work items

The items of one level, in their manual order: the items directly in the collection when parent_id is omitted, and the children of that item when it is given. A whole subtree, and every other filter, is what POST /items:query is for.

Query parameters

NameTypeRequiredDescription
collection_idstring (uuid) required
parent_idstring (uuid) optional
cursorstring optional
sizeinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional
include_archivedboolean
  • default: false
optional
expandarray of string optionalRelations to include, e.g.

Responses

Example bash
curl -X GET "$HUBTASK_URL/items" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

POST /items

Create work item

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Request body application/json — WorkItemCreate

NameTypeRequiredDescription
typeItemType
  • TASK
  • WORK_PACKAGE
  • ACTIVITY
requiredExtensible; /meta/capabilities returns the valid values.
collection_idstring (uuid) optional
parent_idstring (uuid), or null optional
titlestring
  • minLength: 1
  • maxLength: 500
required
notesstring, or null optional
bucket_idstring (uuid), or null optional
before_item_idstring (uuid), or null optional
start_atstring (date-time), or null optional
due_atstring (date-time), or null optional
due_date_onlyboolean
  • default: false
optional
due_time_zonestring, or null optionalAn IANA time zone.
label_idsarray of string (uuid) optional
member_idsarray of string (uuid) optional
assignee_idstring (uuid), or null optional
auto_assignboolean
  • default: false
optional
coverCover optional
kindstring
  • COLOR
  • IMAGE
optional
color_tokenstring, or null optional
media_idstring (uuid), or null optional
custom_fieldsobject optional
content_languagestring, or null optionalBCP-47.
calendar_uidstring, or null
  • minLength: 1
  • maxLength: 255
optionalThe UID a calendar client chose for the entry it is making - what the CalDAV tree sends when a client PUTs a new todo, and what any client may send that will later address the entry through the tree.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "type": "TASK",  "collection_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "parent_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "title": "string",  "notes": "string",  "bucket_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "before_item_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "start_at": "2026-09-16T10:00:00Z",  "due_at": "2026-09-16T10:00:00Z",  "due_date_only": false,  "due_time_zone": "string",  "label_ids": [    "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"  ],  "member_ids": [    "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"  ],  "assignee_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "auto_assign": false,  "cover": {},  "custom_fields": {},  "content_language": "string",  "calendar_uid": "string"}'

GET /items/{itemId}

Get work item

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Query parameters

NameTypeRequiredDescription
expandarray of string optionalRelations to include, e.g.

Responses

Example bash
curl -X GET "$HUBTASK_URL/items/{itemId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

PATCH /items/{itemId}

Update work item

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Request body application/merge-patch+json — WorkItemUpdate

NameTypeRequiredDescription
titlestring
  • minLength: 1
  • maxLength: 500
optional
notesstring, or null optional
bucket_idstring (uuid), or null optional
start_atstring (date-time), or null optional
due_atstring (date-time), or null optionalnull clears the due date, the all-day flag and the zone together - the three fields describe one date and none of them means anything alone.
due_date_onlyboolean optional
due_time_zonestring, or null optionalAn IANA time zone.
assignee_idstring (uuid), or null optional
coverCover optional
kindstring
  • COLOR
  • IMAGE
optional
color_tokenstring, or null optional
media_idstring (uuid), or null optional
custom_fieldsobject optional
content_languagestring, or null optionalBCP-47, and null clears it.

Responses

Example bash
curl -X PATCH "$HUBTASK_URL/items/{itemId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H 'If-Match: "<the ETag you read>"' \  -H "Content-Type: application/merge-patch+json" \  -d '{  "title": "string",  "notes": "string",  "bucket_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "start_at": "2026-09-16T10:00:00Z",  "due_at": "2026-09-16T10:00:00Z",  "due_date_only": true,  "due_time_zone": "string",  "assignee_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "cover": {},  "custom_fields": {},  "content_language": "string"}'

DELETE /items/{itemId}

Trash work item

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Responses

  • 204 Moved to the trash
  • 4XX An error per RFC 9457 Problem
Example bash
curl -X DELETE "$HUBTASK_URL/items/{itemId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H 'If-Match: "<the ETag you read>"'

POST /items:query

The generic query - the basis for list, kanban, and timeline

One filter grammar behind every view. A POST because the query is a document rather than a set of parameters, and it reads: nothing is written, and the same request may be repeated.

The query is always anchored to a scope the caller names, and the permission is checked against that scope once - a refusal is a refusal rather than an empty result. Which fields may be filtered, sorted and grouped, and with which operators, is answered by GET /meta/capabilities under query_fields; a field that is not in that list is refused with query.field_unknown rather than ignored.

Request body application/json — ItemQuery

NameTypeRequiredDescription
scopeobject requiredWhere to look.
container_idstring (uuid) optionalA collection, or a hub - with include_descendants, a hub covers the items of every collection in it.
item_idstring (uuid) optionalAn entry, whose subtree is searched with include_descendants.
include_descendantsboolean
  • default: true
optionalFalse narrows the scope to one level: the entries directly in the collection, or the direct children of the entry.
filterFilterNode optionalEither a leaf (field, op, value) or a combination (op of AND, OR, NOT with nodes).
opstring
  • AND
  • OR
  • NOT
  • EQ
  • NEQ
  • IN
  • NOT_IN
  • LT
  • LTE
  • GT
  • GTE
  • BETWEEN
  • IS_NULL
  • CONTAINS
  • CONTAINS_ANY
  • CONTAINS_ALL
  • STARTS_WITH
  • MATCHES
required
fieldstring optionalOn a leaf, the field to compare.
valueanything optionalOn a leaf, what to compare against - a scalar for most operators, an array for IN, NOT_IN, CONTAINS_ANY, CONTAINS_ALL and BETWEEN (two elements, the bounds), and absent for IS_NULL.
nodesarray of FilterNode optionalOn a combination, the nodes it joins.
opstring
  • AND
  • OR
  • NOT
  • EQ
  • NEQ
  • IN
  • NOT_IN
  • LT
  • LTE
  • GT
  • GTE
  • BETWEEN
  • IS_NULL
  • CONTAINS
  • CONTAINS_ANY
  • CONTAINS_ALL
  • STARTS_WITH
  • MATCHES
required
fieldstring optionalOn a leaf, the field to compare.
valueanything optionalOn a leaf, what to compare against - a scalar for most operators, an array for IN, NOT_IN, CONTAINS_ANY, CONTAINS_ALL and BETWEEN (two elements, the bounds), and absent for IS_NULL.
nodesarray of FilterNode optionalOn a combination, the nodes it joins.
sortarray of SortTerm
  • maxItems: 4
optionalOrdered, most significant first, and always completed by id ASC so that a cursor is unambiguous.
fieldstring required
dirstring
  • ASC
  • DESC
  • default: "ASC"
optional
nullsstring
  • FIRST
  • LAST
  • default: "LAST"
optional
group_byobject optionalThe board projection: one group per distinct value, each with its own rows and its own cursor.
fieldstring required
limit_per_groupinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional
expandarray of string optionalRelations to include, as on GET /items.
pageobject optional
cursorstring, or null optional
sizeinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional
include_archivedboolean
  • default: false
optional
include_trashedboolean
  • default: false
optional
countstring
  • none
  • estimated
  • exact
  • default: "none"
optionalexact counts the whole result with a second query and answers total; it is opt-in because it costs a second pass.

Responses

Codes this operation names

Example bash
curl -X POST "$HUBTASK_URL/items:query" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/json" \  -d '{  "scope": {},  "filter": {    "op": "AND"  },  "sort": [    {      "field": "string"    }  ],  "group_by": {    "field": "string"  },  "expand": [    "string"  ],  "page": {},  "include_archived": false,  "include_trashed": false,  "count": "none"}'

POST /search

Full-text search over the entries a caller may see

Finds entries by the words in their titles and notes, best match first. A POST for the reason the query is one - the request is a document rather than a set of parameters - and it reads: nothing is written, and the same request may be repeated.

Two languages are in play and they are different questions. An entry is indexed under the language it was written in (content_language), so a German entry is found by an inflected German word and an English one by a stemmed English word. The query is read under the caller's language, which is this request's language or the caller's locale. An entry that states no language is matched word by word and is found by any caller. Which languages this installation can index at all is answered by GET /meta/capabilities under text_languages; a language it has no configuration for searches word by word rather than failing.

A script without word boundaries - Japanese, Chinese, Korean, Thai - additionally matches on substrings, because a run of such characters is one token and no word search can find part of it.

Unanchored unless a container is named, and this is the one read of the API that is: it answers what the caller may see rather than refusing what they may not, so a page can be shorter than the size asked for. Walk on until has_more is false rather than stopping at the first short page. Naming a hub is the exception - a client that named a hub it cannot read is told so.

A POST and no GET. What somebody is looking for is their content, and a query string travels through access logs, proxies and browser history; the same words in a body do not. What is searched is entries - tasks, work packages and activities - which is what searchItems says.

Request body application/json — ItemSearchQuery

NameTypeRequiredDescription
qstring
  • minLength: 1
  • maxLength: 200
optionalWhat to look for.
container_idstring (uuid), or null optionalThe hub or collection to search in.
languagestring, or null optionalBCP-47.
modeSearchMode
  • AUTO
  • LEXICAL
  • default: "AUTO"
optionalHow much of the search to use.
filterFilterNode optionalNarrows the search, in the grammar POST /items:query uses: the fields /meta/capabilities names, the operators each one permits, at most five levels of nesting and fifty nodes, and the same cost estimate capped at 50.
opstring
  • AND
  • OR
  • NOT
  • EQ
  • NEQ
  • IN
  • NOT_IN
  • LT
  • LTE
  • GT
  • GTE
  • BETWEEN
  • IS_NULL
  • CONTAINS
  • CONTAINS_ANY
  • CONTAINS_ALL
  • STARTS_WITH
  • MATCHES
required
fieldstring optionalOn a leaf, the field to compare.
valueanything optionalOn a leaf, what to compare against - a scalar for most operators, an array for IN, NOT_IN, CONTAINS_ANY, CONTAINS_ALL and BETWEEN (two elements, the bounds), and absent for IS_NULL.
nodesarray of FilterNode optionalOn a combination, the nodes it joins.
opstring
  • AND
  • OR
  • NOT
  • EQ
  • NEQ
  • IN
  • NOT_IN
  • LT
  • LTE
  • GT
  • GTE
  • BETWEEN
  • IS_NULL
  • CONTAINS
  • CONTAINS_ANY
  • CONTAINS_ALL
  • STARTS_WITH
  • MATCHES
required
fieldstring optionalOn a leaf, the field to compare.
valueanything optionalOn a leaf, what to compare against - a scalar for most operators, an array for IN, NOT_IN, CONTAINS_ANY, CONTAINS_ALL and BETWEEN (two elements, the bounds), and absent for IS_NULL.
nodesarray of FilterNode optionalOn a combination, the nodes it joins.
sortarray of SortTerm
  • maxItems: 4
optionalOnly meaningful without q: with words there is a ranking and it is the ranking.
fieldstring required
dirstring
  • ASC
  • DESC
  • default: "ASC"
optional
nullsstring
  • FIRST
  • LAST
  • default: "LAST"
optional
include_archivedboolean
  • default: false
optional
include_trashedboolean
  • default: false
optional
pageobject optional
cursorstring, or null optional
sizeinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional

Responses

Example bash
curl -X POST "$HUBTASK_URL/search" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/json" \  -d '{  "q": "string",  "container_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "language": "string",  "mode": "AUTO",  "filter": {    "op": "AND"  },  "sort": [    {      "field": "string"    }  ],  "include_archived": false,  "include_trashed": false,  "page": {}}'

POST /search:reindex

Bring the workspace's search documents current

Counts the entries whose search document was built under a text search configuration this installation has since replaced or gained — an installation that upgraded its PostgreSQL, or an operator who installed a configuration, after the entries were written — queues the job that rewrites exactly those in batches, and answers the count. Until it runs, such entries are searched word by word rather than by stem; nothing is lost and nothing is wrong, only shorter.

Requires STRUCTURE at the workspace and the workspace:manage scope: the index is the workspace's, and an administrator decides when it is rebuilt. One job per workspace at a time — asking again while it runs answers the same job. The job's progress is the rows rewritten over the rows that were stale when asked; a second call after it finishes answers stale: 0, which is the report. Auditable, with the count and never a title.

Responses

Example bash
curl -X POST "$HUBTASK_URL/search:reindex" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

POST /items/{itemId}:complete

Complete work item

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Request body application/json

NameTypeRequiredDescription
cascade_childrenboolean
  • default: false
optional

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:complete" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "cascade_children": false}'

POST /items/{itemId}:reopen

Reopen work item

Marks a completed item open again. Idempotent: reopening an item that is already open succeeds and changes nothing. Where the collection's completion policy rolls up, reopening an item also reopens the completed items above it.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:reopen" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)"

POST /items/{itemId}:assign

Assign work item

Gives the entry an assignee. Every type carries one, an activity exactly this and no member list. The account has to be able to see the entry - it has to hold a membership somewhere on the path above it - and one that cannot is refused rather than stored, with the same answer for an account of another tenant and one that does not exist. Idempotent: assigning the person who is already assigned succeeds and announces nothing.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Request body application/json — Assignment

NameTypeRequiredDescription
account_idstring (uuid) requiredThe account to assign.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:assign" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H 'If-Match: "<the ETag you read>"' \  -H "Content-Type: application/json" \  -d '{  "account_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}'

POST /items/{itemId}:unassign

Unassign work item

Takes the assignee off the entry. Idempotent: an entry nobody is assigned to succeeds and announces nothing.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:unassign" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H 'If-Match: "<the ETag you read>"'

POST /items/{itemId}:auto-assign

Auto assign work item

Hands the entry out by the collection's assignment policy - FIXED, RANDOM_MEMBER, RANDOM_GROUP_MEMBER, ROUND_ROBIN or LEAST_LOADED - instead of naming a person. Refused when the collection has no policy configured. Candidates who cannot see the entry are skipped; a policy with nobody eligible leaves the entry as it is and says so in the response's auto_assign object rather than failing. An explicit call uses the policy whether or not it is enabled - enabled decides only whether it applies itself on create.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Responses

  • 200 Handed out by policy, or left as it was with the reason in auto_assign WorkItem
  • 4XX An error per RFC 9457 Problem
Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:auto-assign" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H 'If-Match: "<the ETag you read>"'

POST /items/{itemId}:purge

Purge work item

Removes an entry in the trash and everything under it for good, before its retention period is up. Irreversible: no restore brings it back, and a backup taken before it does not either - the deletion journal sees to that. Refused for an entry that is not in the trash; delete it first, which is reversible. Refused while a legal hold is in force over it, and the answer says at which level.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Responses

  • 204 Removed for good
  • 4XX An error per RFC 9457 Problem
Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:purge" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)"

POST /items/{itemId}:restore

Restore work item

Takes one deletion back out of the trash, whole. Exactly the entries that went in together come back: a separate, younger deletion inside the same subtree stays where it is, because a restore is keyed on the deletion rather than on the subtree. An entry that was archived when it was deleted comes back archived - restoring undoes the deletion and nothing else. Refused while the entry above it, or its collection, is still in the trash; the answer names what to restore first. Idempotent.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:restore" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)"

POST /items/{itemId}:archive

Archive work item

Archives the entry: it is kept and becomes read-only. Distinct from the trash, which is a deletion with a retention period running against it - an archived entry stays until somebody says otherwise. Nothing below it is archived with it: an entry's children are entries in their own right, unlike the collections of a hub. Idempotent - archiving an archived entry succeeds and changes nothing.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:archive" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)"

POST /items/{itemId}:unarchive

Unarchive work item

Makes an archived entry writable again. Refused for an entry in the trash: restore it first, and the answer says so. Idempotent.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:unarchive" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)"

POST /items/{itemId}:reorder

Reorder work item

Moves the item within its own level. The position is given as the item to place it before; omitting it appends to the end. The rank is a fractional index, so an insertion between two neighbours renumbers nothing else and two offline devices can insert into the same list without either one's order being discarded.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Request body application/json

NameTypeRequiredDescription
before_item_idstring (uuid), or null optionalThe sibling to place this item before.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:reorder" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "before_item_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}'

POST /items/{itemId}:move

Move work item

Moves the item and its subtree; checks the capability profile and reference resolution.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Request body application/json

NameTypeRequiredDescription
target_parent_idstring (uuid), or null optionalThe item to move this one under.
target_collection_idstring (uuid) optionalThe destination collection.
target_bucket_idstring (uuid), or null optional
before_item_idstring (uuid), or null optionalThe sibling to place this item before at the destination.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:move" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "target_parent_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "target_collection_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "target_bucket_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "before_item_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}'

POST /items/{itemId}:duplicate

Duplicate work item

Copies the entry, and everything below it when include_subtree is set. The copy is a new entry rather than a fork of a conversation: the description travels - the notes, the labels, the members, the assignee, the attachments and the custom field values, as far as the capability profile of each type allows - and the comments and the history do not.

What the destination cannot resolve is reported rather than dropped in silence, exactly as a move reports it (invariant I-W6): a copy into another collection leaves behind the labels of the vocabulary it came from, the column of the board it sat on, and the values of custom fields that collection does not define.

The copy is open and carries fresh ranks at the end of its level; an entry that was archived below the copied one stays archived in the copy, and trashed entries are not copied at all. The title is copied unchanged - a server that invented "Copy of …" would be writing display text - so a client that wants a different one sends it.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Request body application/json

NameTypeRequiredDescription
include_subtreeboolean
  • default: false
optionalWhether everything below the entry is copied with it.
target_parent_idstring (uuid), or null optionalThe entry the copy sits under.
target_collection_idstring (uuid) optionalThe destination collection.
titlestring
  • minLength: 1
  • maxLength: 500
optionalThe title of the copy.

Responses

Example bash
curl -X POST "$HUBTASK_URL/items/{itemId}:duplicate" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "include_subtree": false,  "target_parent_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "target_collection_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "title": "string"}'

POST /items:bulk

Bulk update work items

Up to 500 operations, each of them one of the operations this API offers on an entry singly, applied in the order they are sent. The answer is HTTP 200 with a result per operation, and the status of each operation is in that result: a bulk that half succeeded is not a failed request, and a client reads what happened per operation rather than from one status covering 500 of them.

An operation applies exactly as it would on its own - its own permission check, its own event, its own entry in the item's history and its own per-field records for offline clients. One event covering 500 entries would break a merge in a way nothing downstream recovers from, and one history entry for a bulk hides what the history exists to show.

atomic: true makes it all or nothing: the first refusal rolls the whole bulk back and every result reports what happened, the refusal in its place and the rest as not applied. Without it, the operations that succeeded stay applied and the ones that failed carry their problem.

Headers

NameTypeRequiredDescription
Idempotency-Keystring (uuid) optionalA UUID; identical requests return the same result for 24 h.

Request body application/json

NameTypeRequiredDescription
atomicboolean
  • default: false
optionalAll or nothing.
operationsarray of BulkOperation
  • maxItems: 500
required
opstring
  • CREATE_ITEM
  • UPDATE_ITEM
  • COMPLETE_ITEM
  • REOPEN_ITEM
  • MOVE_ITEM
  • TRASH_ITEM
  • ADD_LABEL
  • REMOVE_LABEL
  • ASSIGN
requiredThe operation.
item_idstring (uuid) optionalThe entry the operation is about.
payloadobject optionalWhat the single-entry operation takes in its body, by the field names it takes there.

Responses

  • 200 The result per operation, in the order the operations were sent.
  • 4XX An error per RFC 9457 Problem
Example bash
curl -X POST "$HUBTASK_URL/items:bulk" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{  "atomic": false,  "operations": [    {      "op": "CREATE_ITEM"    }  ]}'

GET /items/{itemId}/activity

List activity

The entry's own history, newest first: who did what, and when. It is the product's record and not the audit trail - it is readable by whoever may read the entry, and it is deleted with the entry. Each step carries a message code rather than a sentence, and the client renders it. For an activity the history is compact: the code, the actor and the time, without the fields that moved.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Query parameters

NameTypeRequiredDescription
cursorstring optional
sizeinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional

Responses

Example bash
curl -X GET "$HUBTASK_URL/items/{itemId}/activity" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

PUT /items/{itemId}/labels/{labelId}

Add label

Puts a label on an entry. Idempotent: an entry that already carries it succeeds. The label has to belong to the entry's own collection - a label is a vocabulary one collection agreed on - and the entry's type has to carry the LABELS capability.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required
labelIdstring (uuid) required

Responses

Example bash
curl -X PUT "$HUBTASK_URL/items/{itemId}/labels/{labelId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

DELETE /items/{itemId}/labels/{labelId}

Remove label

Takes a label off an entry. Idempotent: an entry that does not carry it succeeds.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required
labelIdstring (uuid) required

Responses

Example bash
curl -X DELETE "$HUBTASK_URL/items/{itemId}/labels/{labelId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

PUT /items/{itemId}/members/{accountId}

Add member

Puts an account on an entry's member list. Idempotent: an entry that already carries it succeeds. The account has to be able to see the entry - it has to hold a membership somewhere on the path above it - and the entry's type has to carry the MEMBERS capability, which an activity does not: an activity has one assignee and no member list.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required
accountIdstring (uuid) requiredAn account identifier, always a UUID.

Responses

Example bash
curl -X PUT "$HUBTASK_URL/items/{itemId}/members/{accountId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

DELETE /items/{itemId}/members/{accountId}

Remove member

Takes an account off an entry's member list. Idempotent: an entry that does not carry it succeeds.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required
accountIdstring (uuid) requiredAn account identifier, always a UUID.

Responses

Example bash
curl -X DELETE "$HUBTASK_URL/items/{itemId}/members/{accountId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

PUT /items/{itemId}/custom-fields/{key}

Set custom field

Writes one custom field on an entry. The value is validated against the definition in force - a SELECT value outside its options, a NUMBER arriving as a string, a USER pointing at an account of another workspace are each validation_failed with the field path, never a stored value. Sending null clears the key. One key per call, because the merge rule is per key: two devices setting two different keys converge to both.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required
keystring
  • pattern: ^[a-z][a-z0-9_]{0,49}$
requiredThe definition's key.

Headers

NameTypeRequiredDescription
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Request body application/json — CustomFieldValue

NameTypeRequiredDescription
valueanything requiredThe value, shaped by the definition's kind: a string for TEXT, SELECT and URL, a number for NUMBER, a boolean for BOOL, an ISO-8601 date for DATE, an account identifier for USER, and an array of strings for MULTI_SELECT.

Responses

Example bash
curl -X PUT "$HUBTASK_URL/items/{itemId}/custom-fields/{key}" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H 'If-Match: "<the ETag you read>"' \  -H "Content-Type: application/json" \  -d '{  "value": "string"}'

PUT /items/{itemId}/cover

Set cover

Puts a cover on the entry: a colour token, or an image by its media identifier. Only a type whose profile carries the COVER capability has one - by default a task, and neither a work package nor an activity. An image has to be a READY media object of usage COVER in this workspace. Replacing a cover is this call again; the displaced image loses a reference and is cleaned up by the reconciliation job once nothing points at it.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Request body application/json — CoverInput

NameTypeRequiredDescription
kindstring
  • COLOR
  • IMAGE
required
color_tokenstring, or null optional
media_idstring (uuid), or null optional

Responses

Example bash
curl -X PUT "$HUBTASK_URL/items/{itemId}/cover" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H 'If-Match: "<the ETag you read>"' \  -H "Content-Type: application/json" \  -d '{  "kind": "COLOR",  "color_token": "string",  "media_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}'

DELETE /items/{itemId}/cover

Clear cover

Takes the cover off the entry. Idempotent: an entry with no cover succeeds and announces nothing.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Responses

Example bash
curl -X DELETE "$HUBTASK_URL/items/{itemId}/cover" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H 'If-Match: "<the ETag you read>"'

PUT /items/{itemId}/due

Set due date

Puts a due date on the entry: the instant, the all-day flag, and the IANA zone, written together because the three describe one date. An all-day due date is a date in the given zone, never a midnight that shifts with the viewer. A zone that is not an IANA name is refused. Moving a due date is this call again. The same three fields on the create and update paths dispatch into this writer, so a due date means the same thing whichever door it came through.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Request body application/json — DueDateInput

NameTypeRequiredDescription
due_atstring (date-time) required
due_date_onlyboolean
  • default: false
optional
due_time_zonestring, or null optionalAn IANA time zone, such as Europe/Berlin.

Responses

Example bash
curl -X PUT "$HUBTASK_URL/items/{itemId}/due" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H 'If-Match: "<the ETag you read>"' \  -H "Content-Type: application/json" \  -d '{  "due_at": "2026-09-16T10:00:00Z",  "due_date_only": false,  "due_time_zone": "string"}'

DELETE /items/{itemId}/due

Clear due date

Takes the due date off the entry - the instant, the all-day flag and the zone together, because none of them means anything alone. Idempotent: an entry with no due date succeeds and announces nothing.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Headers

NameTypeRequiredDescription
If-Matchstring optionalThe ETag of the state last read (optimistic locking).

Responses

Example bash
curl -X DELETE "$HUBTASK_URL/items/{itemId}/due" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H 'If-Match: "<the ETag you read>"'

GET /items/{itemId}/attachments

List attachments

The entry's attachments as media objects, oldest first. Readable by whoever may read the entry.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required

Query parameters

NameTypeRequiredDescription
cursorstring optional
sizeinteger
  • default: 50
  • minimum: 1
  • maximum: 200
optional

Responses

Example bash
curl -X GET "$HUBTASK_URL/items/{itemId}/attachments" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

PUT /items/{itemId}/attachments/{mediaId}

Attach media

Attaches a media object to the entry. Idempotent: an entry that already carries it succeeds. The object has to be READY, of usage ATTACHMENT and of this workspace, and the entry's type has to carry the ATTACHMENTS capability - an activity does not. Attaching raises the object's reference count; the bytes are shared, never copied.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required
mediaIdstring (uuid) required

Responses

Example bash
curl -X PUT "$HUBTASK_URL/items/{itemId}/attachments/{mediaId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

DELETE /items/{itemId}/attachments/{mediaId}

Detach media

Takes a media object off the entry. Idempotent: an entry that does not carry it succeeds. The object itself stays; once nothing references it, the reconciliation job removes it.

Path parameters

NameTypeRequiredDescription
itemIdstring (uuid) required
mediaIdstring (uuid) required

Responses

Example bash
curl -X DELETE "$HUBTASK_URL/items/{itemId}/attachments/{mediaId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"