API reference · Hubtask API 0.1.0
items
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
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
Responses
200What the pass removed PurgeSummary4XXAn error per RFC 9457 Problem
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
Responses
200A page of items WorkItemPage4XXAn error per RFC 9457 Problem
curl -X GET "$HUBTASK_URL/items" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /items
Create work item
Headers
Request body application/json — WorkItemCreate
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
PATCH /items/{itemId}
Update work item
Path parameters
Headers
Request body application/merge-patch+json — WorkItemUpdate
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
Headers
Responses
204Moved to the trash4XXAn error per RFC 9457 Problem
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
Responses
200The result, optionally grouped ItemQueryResult422An error per RFC 9457 Problem4XXAn error per RFC 9457 Problem
Codes this operation names
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
Responses
200The hits, best match first WorkItemPage422An error per RFC 9457 Problem4XXAn error per RFC 9457 Problem
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
202The rebuild was accepted SearchReindex403An error per RFC 9457 Problem
curl -X POST "$HUBTASK_URL/search:reindex" \ -H "Authorization: Bearer $HUBTASK_TOKEN" POST /items/{itemId}:complete
Complete work item
Path parameters
Headers
Request body application/json
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
Headers
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
Headers
Request body application/json — Assignment
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
Headers
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
Headers
Responses
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
Headers
Responses
204Removed for good4XXAn error per RFC 9457 Problem
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
Headers
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
Headers
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
Headers
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
Headers
Request body application/json
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
Headers
Request body application/json
Responses
200Moved MoveResult4XXAn error per RFC 9457 Problem
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
Headers
Request body application/json
Responses
201Duplicated DuplicateResult4XXAn error per RFC 9457 Problem
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
Request body application/json
Responses
200The result per operation, in the order the operations were sent.4XXAn error per RFC 9457 Problem
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
Query parameters
Responses
200The history ActivityPage4XXAn error per RFC 9457 Problem
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
Responses
200The labels the entry now carries ItemLabels4XXAn error per RFC 9457 Problem
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
Responses
200The labels the entry now carries ItemLabels4XXAn error per RFC 9457 Problem
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
Responses
200The members the entry now carries ItemMembers4XXAn error per RFC 9457 Problem
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
Responses
200The members the entry now carries ItemMembers4XXAn error per RFC 9457 Problem
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
Headers
Request body application/json — CustomFieldValue
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
Headers
Request body application/json — CoverInput
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
Headers
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
Headers
Request body application/json — DueDateInput
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
Headers
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
Query parameters
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
Responses
200The attachments the entry now carries ItemAttachments4XXAn error per RFC 9457 Problem
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
Responses
200The attachments the entry now carries ItemAttachments4XXAn error per RFC 9457 Problem
curl -X DELETE "$HUBTASK_URL/items/{itemId}/attachments/{mediaId}" \ -H "Authorization: Bearer $HUBTASK_TOKEN"