{"openapi":"3.1.0","info":{"title":"TaskNeuron Public API","version":"1.0.0","description":"Programmatic access to your projects and tasks. Authenticate with an API key (Settings → API keys) as a Bearer token. Rate limited per key."},"servers":[{"url":"/api/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key as `Authorization: Bearer <key>`"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"code":{"type":"string"}},"required":["message"]}}},"Pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"returned":{"type":"integer"},"hasMore":{"type":"boolean"},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `?cursor=` to fetch the next page. Null on the last page."}}},"Project":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"status":{"type":"string"},"projectType":{"type":"string"},"itemKey":{"type":"string","description":"Work item key prefix: \"TN\" makes task 123 \"TN-123\""},"codeMergeStatus":{"type":"string","nullable":true,"enum":["none","in_progress","review","completed"],"description":"Status linked work items move to when their pull request merges; null = default"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Task":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"parentTaskId":{"type":"string","nullable":true},"itemNumber":{"type":"integer","description":"Number within the project; with the project's itemKey, e.g. TN-123"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"status":{"type":"string","enum":["todo","in_progress","review","completed"]},"priority":{"type":"string","enum":["low","medium","high","urgent"]},"estimatedHours":{"type":"integer","nullable":true},"dueDate":{"type":"string","format":"date-time","nullable":true},"phase":{"type":"string","nullable":true},"workItemType":{"type":"string","enum":["epic","feature","story","bug","task","issue"]},"storyPoints":{"type":"number","nullable":true},"originalEstimate":{"type":"number","nullable":true,"description":"Hours"},"remainingWork":{"type":"number","nullable":true,"description":"Hours"},"completedWork":{"type":"number","nullable":true,"description":"Hours"},"severity":{"type":"string","enum":["critical","high","medium","low"],"nullable":true},"reproSteps":{"type":"string","nullable":true},"acceptanceCriteriaText":{"type":"string","nullable":true},"customFields":{"type":"object","description":"Values (string, number or boolean) keyed by custom field key; see /custom-fields"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"WorkItemFields":{"type":"object","description":"Optional on create and update; clients that send none of them get a plain `task` and keep working. Nullable fields take null to clear. customFields merges by key on update and null clears a key; values are validated against the workspace's custom field definitions for the task's type, required ones included (400 invalid_custom_fields lists every problem). Edits that leave the type and custom values alone are not re-validated, so a field made required later never blocks them. A type change must fit the parent and children (400 invalid_work_item_type): epic → feature, task; feature → story, bug, issue, task; story → task, bug; bug → task; issue → task; task → task, bug, issue. Out-of-range values are 400 invalid_input.","properties":{"workItemType":{"type":"string","enum":["epic","feature","story","bug","task","issue"],"default":"task"},"storyPoints":{"type":"number","minimum":0,"maximum":100000,"nullable":true},"originalEstimate":{"type":"number","minimum":0,"maximum":100000,"nullable":true,"description":"Hours"},"remainingWork":{"type":"number","minimum":0,"maximum":100000,"nullable":true,"description":"Hours"},"completedWork":{"type":"number","minimum":0,"maximum":100000,"nullable":true,"description":"Hours"},"severity":{"type":"string","enum":["critical","high","medium","low"],"nullable":true},"reproSteps":{"type":"string","maxLength":20000,"nullable":true},"acceptanceCriteriaText":{"type":"string","maxLength":20000,"nullable":true},"customFields":{"type":"object","description":"Key → string, number, boolean, or null to clear. Merges by key on update."}}},"Rollup":{"type":"object","nullable":true,"description":"Totals over all of a task's descendants (the task itself excluded); null for a task with none. Sums are null when no descendant has a value.","properties":{"descendantCount":{"type":"integer"},"completedCount":{"type":"integer"},"percentComplete":{"type":"number","nullable":true},"storyPoints":{"type":"number","nullable":true},"remainingWork":{"type":"number","nullable":true},"completedWork":{"type":"number","nullable":true},"estimatedHours":{"type":"number","nullable":true}}},"CustomField":{"type":"object","properties":{"id":{"type":"string"},"workspaceId":{"type":"string"},"key":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,63}$"},"label":{"type":"string"},"kind":{"type":"string","enum":["text","number","date","picklist","person","boolean"]},"appliesTo":{"type":"array","items":{"type":"string","enum":["epic","feature","story","bug","task","issue"]},"description":"Empty = every type"},"options":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}}}},"required":{"type":"boolean"},"position":{"type":"integer"},"archivedAt":{"type":"string","format":"date-time","nullable":true}}}}},"paths":{"/me":{"get":{"summary":"Current account","responses":{"200":{"description":"The authenticated account"}}}},"/projects":{"get":{"summary":"List projects","parameters":[{"name":"view","in":"query","schema":{"type":"string","enum":["list","tree"],"default":"list"},"description":"`tree` (needs projectId) returns `{ tree, taskCount }`: the project's active tasks nested to any depth, each node with `level`, `rollup` and `children`. Not paginated; status, workItemType and cursor are rejected in this mode."},{"name":"rootTaskId","in":"query","schema":{"type":"string"},"description":"With view=tree: return only this task's branch."},{"name":"limit","in":"query","schema":{"type":"integer","maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer"},"deprecated":true,"description":"Legacy paging. Lossy: the list is ordered by a column concurrent writes can move, so a row updated between requests shifts past the offset and is returned by no page. Prefer `cursor`."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque page token from the previous response's `pagination.nextCursor`. Anchors the next page to a row rather than a count, so concurrent writes cannot shift the boundary. Supersedes `offset`. Keep paging while `nextCursor` is non-null."}],"responses":{"200":{"description":"Projects + pagination"}}},"post":{"summary":"Create a project","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"projectType":{"type":"string"}}}}}},"responses":{"201":{"description":"Created project"}}}},"/projects/{id}":{"patch":{"summary":"Update a project","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"At least one field is required; an empty patch is rejected.","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"goal":{"type":"string","nullable":true},"summary":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","archived","completed"]},"projectType":{"type":"string"},"primaryLocation":{"type":"string","nullable":true},"targetDate":{"type":"integer","nullable":true,"description":"Epoch milliseconds; null clears the target date."},"itemKey":{"type":"string","pattern":"^[A-Z][A-Z0-9]{1,9}$","description":"Work item key prefix (lowercase is uppercased). Keys in older commits stop resolving after a change. People only (agents get 403 human_required); audited."},"codeMergeStatus":{"type":"string","nullable":true,"enum":["none","in_progress","review","completed"],"description":"Move linked work items to this status when their pull request merges; none = never; null = default (the PR-status capability, when enabled, completes them). People only; audited."}}}}}},"responses":{"200":{"description":"Updated project"}}}},"/tasks":{"get":{"summary":"List tasks","parameters":[{"name":"projectId","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"workItemType","in":"query","schema":{"type":"string","enum":["epic","feature","story","bug","task","issue"]}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer"},"deprecated":true,"description":"Legacy paging. Lossy: the list is ordered by a column concurrent writes can move, so a row updated between requests shifts past the offset and is returned by no page. Prefer `cursor`."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque page token from the previous response's `pagination.nextCursor`. Anchors the next page to a row rather than a count, so concurrent writes cannot shift the boundary. Supersedes `offset`. Keep paging while `nextCursor` is non-null."}],"responses":{"200":{"description":"Tasks (each with `rollup`) + pagination, or the tree with view=tree"}}},"post":{"summary":"Create a task","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["projectId","title"],"description":"Plus the optional WorkItemFields (type, agile fields, customFields).","properties":{"projectId":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"priority":{"type":"string","enum":["low","medium","high","urgent"]},"estimatedHours":{"type":"integer"},"dueDate":{"type":"integer","description":"Epoch milliseconds"},"phase":{"type":"string"},"workItemType":{"type":"string","enum":["epic","feature","story","bug","task","issue"]},"storyPoints":{"type":"number","minimum":0},"severity":{"type":"string","enum":["critical","high","medium","low"]},"customFields":{"type":"object","description":"Key → value; validated against /custom-fields"}}}}}},"responses":{"201":{"description":"Created task"}}}},"/tasks/{id}":{"patch":{"summary":"Update a task","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated task"},"404":{"description":"Not found"}}},"delete":{"summary":"Delete a task (soft, recoverable)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/tasks/{id}/move":{"post":{"summary":"Move a task and its whole branch","description":"Re-parents the task under any allowed parent in the same project, or to the top level with null. Its children, grandchildren and so on move with it. Hierarchy rules apply (no cycle, the parent's type must accept the task's type, the branch must fit the maximum depth); a violation returns 400 or 409 `invalid_parent` with the rule in the message. Optional phase and workstreamId are applied to the whole branch.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["parentTaskId"],"properties":{"parentTaskId":{"type":"string","nullable":true},"phase":{"type":"string","nullable":true},"workstreamId":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"task (with ancestors, level, childSummary, rollup) and movedDescendantIds"},"400":{"description":"Rule violation or invalid input"},"404":{"description":"Not found"},"409":{"description":"Cycle or too deep"}}}},"/custom-fields":{"get":{"summary":"List custom field definitions","description":"The caller's workspace, or a team's with ?teamId. Agents may read.","parameters":[{"name":"teamId","in":"query","schema":{"type":"string"}},{"name":"includeArchived","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"{ customFields: CustomField[] }"}}},"post":{"summary":"Define a custom field","description":"People only (agents get 403 human_required); a team's fields need team owner or admin. Audited.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key","label","kind"],"properties":{"teamId":{"type":"string"},"key":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,63}$"},"label":{"type":"string"},"kind":{"type":"string","enum":["text","number","date","picklist","person","boolean"]},"appliesTo":{"type":"array","items":{"type":"string"}},"options":{"type":"array","items":{"type":"object"}},"required":{"type":"boolean"},"position":{"type":"integer"}}}}}},"responses":{"201":{"description":"Created"},"403":{"description":"Not allowed"},"409":{"description":"Key in use"}}}},"/custom-fields/{id}":{"patch":{"summary":"Update, archive or restore a custom field","description":"label, appliesTo, options, required, position, archived. key and kind cannot change. People only; audited.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"},"400":{"description":"Invalid or immutable field"}}},"delete":{"summary":"Archive a custom field (stored values are kept)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Archived"}}}},"/tasks/{id}/context":{"get":{"summary":"Full working context for a task (parent, subtasks, project goal, workstream)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"task (with itemKey, ancestors root first, level, childSummary by status and type, and rollup), parent, subtasks, workstream, project (goal/summary/recommendations), codeLinks (branches, commits, pull requests and builds from GitHub, newest first, at most 50) and codeLinkCount"},"404":{"description":"Not found"}}}},"/tasks/search":{"get":{"summary":"Search tasks (DB-side substring, paginated)","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}},{"name":"projectId","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["todo","in_progress","review","completed"]}},{"name":"priority","in":"query","schema":{"type":"string","enum":["low","medium","high","urgent"]}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer"},"deprecated":true,"description":"Legacy paging. Lossy: the list is ordered by a column concurrent writes can move, so a row updated between requests shifts past the offset and is returned by no page. Prefer `cursor`."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque page token from the previous response's `pagination.nextCursor`. Anchors the next page to a row rather than a count, so concurrent writes cannot shift the boundary. Supersedes `offset`. Keep paging while `nextCursor` is non-null."}],"responses":{"200":{"description":"Matching tasks + pagination"}}}},"/next-task":{"get":{"summary":"The single highest-priority actionable task","parameters":[{"name":"projectId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"{ task: Task | null }"}}}},"/projects/{id}/structure":{"get":{"summary":"Project hierarchy (phases → tasks → subtasks, nested to any depth, each with rollup) + derived views","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"project, phases, totals, actionable, actionableCount, next, taskCount"},"404":{"description":"Not found"}}}},"/projects/{id}/map":{"get":{"summary":"Project as a Cortex map graph (nodes + hierarchy and dependency edges)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"project, nodes, edges, counts (ProjectMapGraph)"},"404":{"description":"Not found"}}}},"/projects/{id}/tnl":{"get":{"summary":"Project as a Taskneol (.tnl) file: canonical form with task and stream ids, briefs and dependencies","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json"]},"description":"json returns { projectId, fileName, tnl, losses } instead of the file"}],"responses":{"200":{"description":"The .tnl file as a download (X-Taskneol-Losses: how many things it couldn't hold), or JSON with format=json"},"404":{"description":"Not found"}}}},"/taskneol/spec":{"get":{"summary":"The Taskneol (.tnl) language spec, as markdown","responses":{"200":{"description":"text/markdown"}}}},"/plans":{"post":{"summary":"Start an async AI plan generation","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["goal"],"properties":{"goal":{"type":"string","minLength":3,"maxLength":8000},"projectType":{"type":"string"},"location":{"type":"string"}}}}}},"responses":{"202":{"description":"Job accepted — returns { jobId, status }"},"403":{"description":"Email not verified"},"429":{"description":"Too many active jobs"}}}},"/plans/{id}":{"get":{"summary":"Poll a plan-generation job (status/progress/result)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job view (status, stage, progress, result when done)"},"404":{"description":"Not found"}}}},"/projects/from-plan":{"post":{"summary":"Create a project from a completed plan job","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jobId"],"properties":{"jobId":{"type":"string"}}}}}},"responses":{"201":{"description":"Created project"},"404":{"description":"Job not found"},"409":{"description":"Plan job not finished"}}}},"/conversations":{"post":{"summary":"Append an external assistant's chat transcript (Claude, ChatGPT…) to a conversation","description":"Omit conversationId to start a conversation; pass the returned id on later calls so a continued chat keeps appending. Messages already stored are skipped. Messages over 20,000 characters are cut, and a conversation holds up to 1,000,000 characters.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["messages"],"properties":{"conversationId":{"type":"string"},"client":{"type":"string","description":"Assistant name, e.g. claude or chatgpt"},"title":{"type":"string"},"projectId":{"type":"string","description":"Project this chat is about"},"taskIds":{"type":"array","maxItems":50,"items":{"type":"string"}},"messages":{"type":"array","maxItems":200,"items":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Messages appended — { conversationId, added, duplicates, … }"},"201":{"description":"Conversation created — { conversationId, added, duplicates, … }"},"404":{"description":"Conversation not found"}}},"get":{"summary":"Your conversations about a project (or its tasks) or a task, with recent messages","parameters":[{"name":"projectId","in":"query","schema":{"type":"string"}},{"name":"taskId","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":40}}],"responses":{"200":{"description":"{ conversations: [{ id, origin, client, title, messages, … }] }, newest last"},"404":{"description":"Project or task not found"}}}}}}