Documentation

TaskOH assistant access: reference for AI agents

Status
As built (personal access tokens only; OAuth not available)
Version
1.5
Date
2026-09-07
Applies to
https://api.taskoh.app/mcp (MCP server) · https://api.taskoh.app/openapi.json (REST) · https://app.taskoh.app (web app)
Related docs
AI assistants (people guide: creating tokens, client setup) · llms.txt (plain-text reference for agents) · openapi.json (REST schema)

This document is for an LLM-based agent that holds a TaskOH token and must act for a user.

1. Purpose and mental model

  • TaskOH is a project management app. Hierarchy: workspace > projects > tasks (> subtasks, comments).
  • A token belongs to one user. Every call runs as that user, with that user's role in each project. There are no token scopes.
  • Project roles: admin, project_manager, guest. admin and project_manager are "the project team". guest is a client: sees only client_visible tasks, cannot edit or delete tasks, cannot set labels.
  • Writes that produce activity events (task_created, status_changed, scope_changed, billing_changed, billing_visibility_changed, comment_added, task_deleted) are recorded in the TaskOH activity log with metadata.via = "api"; the web app shows them as "via API". Other field edits (title, description, priority, due date, assignee, labels, parent, client_visible) are not logged — see §5.6.
  • Fixed vocabularies:
FieldValues
statusinbox, planned, in_progress, in_review, needs_client, done
typerequest, bug, task
priority1 low, 2 medium, 3 high (or null)
scopeunclassified, in_scope, out_of_scope
  • "Open" means status is not done. search_tasks has no open/closed switch: pass "status":["inbox","planned","in_progress","in_review","needs_client"] for open tasks (as in §7.5) or ["done"] for closed ones.

1.1 Minimum successful session

  1. get_context (no arguments) → learn me, your projects and roles.
  2. If you will name a member or label: get_context with project.
  3. Read: get_my_tasks (own open work) or search_tasks (needs project) or get_task.
  4. Write: create_task / update_task / add_comment; delete_task only after explicit user consent (§7.8).
  5. Report the returned url. Full rules: §8. Recipes: §7.

2. Authentication

2.1 Header

Authorization: Bearer toh_pat_<64 lowercase hex characters>
  • Scheme is case-insensitive; the token is 72 characters, prefix toh_pat_.
  • The same header works on POST /mcp and on the REST operations listed in §9; the session-only endpoints in §2.3 return 403.

2.2 Failure responses

Authentication is checked before the controller on POST /mcp and on every route in the protected /api group (the seven REST operations in §9 are all in it); GET/DELETE /mcp answer 405 without checking the token (see §3.1). The body is always the REST envelope, even on /mcp.

HTTPHeaderBodyCause
401WWW-Authenticate: Bearer{"success":false,"error":{"type":"unauthorized","message":"Missing Authorization header","code":401}}no header
401WWW-Authenticate: Bearersame shape, message Invalid Authorization header formatheader does not match Bearer <token>
401WWW-Authenticate: Bearersame shape, message Invalid or revoked tokenbearer starts with toh_ but the token is unknown, revoked, or expired
401WWW-Authenticate: Bearersame shape, message Invalid tokenbearer does not start with toh_ (the token was pasted without its toh_pat_ prefix)
403none{"success":false,"error":{"type":"forbidden","message":"This route requires an interactive session","code":403}}valid token on a session-only route
403server: cloudflare, content-type: text/plainerror code: 1010 (plain text, no JSON envelope)answered by the CDN edge, not by TaskOH: a zone security rule rejected the request. Since 2026-09-07 the API host no longer applies browser-signature checks (Python's default Python-urllib/3.x works), so this should not occur in normal operation. Not an authentication problem.
429Retry-After: 60{"success":false,"error":{"type":"rate_limited","message":"Too many requests. Please try again later.","code":429}}more than 120 requests in 60 s with this token — one bucket per token, shared by every /api route and POST /mcp; a JSON-RPC batch of n messages costs n requests and is refused as a whole when it does not fit. Not an authentication problem.
  • A 401 with Invalid or revoked token is final for that token. Do not retry. Tell the user to create a new token at https://app.taskoh.app/integrations (sign in first; the page is reachable by every role, including clients).
  • Revocation and expiry take effect on the next request (no caching).
  • A plain-text 403 with the body error code: 1010 never comes from TaskOH: it is Cloudflare answering before the request reaches the API. Until 2026-09-07 the edge rejected Python's default Python-urllib/3.x User-Agent this way; the API host (api.taskoh.app) no longer applies that check, while the browser-facing hosts taskoh.app and app.taskoh.app still do. If you ever see it on the API host, retry after a short wait and report the cf-ray response header to the TaskOH team — the token is fine. Sending a descriptive User-Agent such as my-assistant/1.0 (+https://example.com) remains good practice.
  • A 429 is not a token problem: the token is neither revoked nor otherwise affected. Wait the number of seconds given in Retry-After (60), then retry, and reduce call volume: paginate search_tasks with page/per_page (max 100), send one update_task with several fields instead of several calls, and call get_context once per session. The quota is 120 requests per rolling 60-second window per token (not per IP address), counted across every /api route and POST /mcp combined; a JSON-RPC batch of n messages (max 20) costs n requests, and a batch that does not fit in the remaining quota is refused as a whole with the same HTTP 429 envelope and Retry-After: 60 — none of its messages is executed. On /mcp the 429 is a plain HTTP response with the JSON envelope above, not a JSON-RPC error object. Browser sessions (the web app's session JWT) calling POST /mcp share the same 120-per-60 s bucket, keyed per user; sessions on the /api REST routes are not limited, and the sign-in endpoints keep their separate limit of 10 per 15 minutes per IP address.

2.3 Session-only endpoints (403 for tokens)

A token receives the 403 above on endpoints that mint credentials, grant durable access to other people, or take irreversible or financial actions: token management (listing, creating, revoking), profile and password changes, adding or removing project members, workspace team management (removing a person, resending an invitation), deleting a project, and Stripe checkout or billing-portal sessions. Read-only lists (GET /api/team, GET /api/subscription) stay available. None of these are part of the documented agent operations (§5, §9). Project payloads returned to a token (GET /api/projects, GET /api/projects/{id}, POST /api/projects) omit the project's public share-link fields.

2.4 Token hygiene

  • Never log, echo, quote, or store the token. Never include it in a tool result, comment body, or task text.
  • Rejected tokens are logged server-side with their last 4 characters, the client IP address and the request path — never the token.
  • Never try to create tokens: the account owner is e-mailed on every token creation and at most 20 active tokens exist per user (token management is session-only anyway, §2.3).

3. Choosing a transport

MCP (preferred)REST
URLPOST https://api.taskoh.app/mcphttps://api.taskoh.app/api/..., schema at https://api.taskoh.app/openapi.json
FormatJSON-RPC 2.0, JSON-only Streamable HTTPJSON, envelope {"success":true,"data":...}
Name resolutionproject, assignee and label names acceptednumeric ids only
Operations8 tools7 operations
Use whenthe client supports MCP (Claude Code, Claude Desktop via mcp-remote, Claude API, OpenAI Responses/Agents SDK)the client is a ChatGPT Custom GPT Action or a plain HTTP script

3.1 MCP transport facts

FactValue
MethodPOST only. GET and DELETE return HTTP 405 with Allow: POST and an empty body, before and without authentication; other verbs are answered by the app's JSON error handler with HTTP 405 (body shape not part of the contract)
Headers to sendAuthorization, Content-Type: application/json. MCP-Protocol-Version is optional
Supported protocol versions2025-03-26, 2025-06-18, 2025-11-25 (default 2025-11-25)
MCP-Protocol-Version headerif present it must match YYYY-MM-DD, otherwise HTTP 400 with JSON-RPC error -32600 Malformed MCP-Protocol-Version header. Any date-shaped value is accepted
Sessionsnone. No Mcp-Session-Id header is ever emitted or required. initialize is optional; tools/list and tools/call work without it
Server-sent eventsnot supported. Use --transport http (Claude Code) or --transport http-only (mcp-remote)
Batcha non-empty JSON array of up to 20 requests returns a JSON array of replies (HTTP 200). Empty array [] returns -32600 Invalid Request; more than 20 elements returns -32600 Batch too large (max 20). Each message costs one request of the rate-limit quota (§2.2): a batch that does not fit in the remaining quota is refused as a whole with HTTP 429, and none of its messages is executed
Request ida string, a number or null; any other type answers -32600 Invalid Request
Notificationsa message without id, or whose method starts with notifications/, gets no reply. If the whole request was notifications the response is HTTP 202 with an empty body
Unknown method-32601 Method not found (HTTP 200). server/discover is not implemented; fall back to initialize
Methods implementedinitialize, ping, tools/list, tools/call
Response Content-Typealways application/json
HTTP status of JSON-RPC errors200 except the 400 above and the 405

A complete request:

POST /mcp HTTP/1.1
Host: api.taskoh.app
Authorization: Bearer toh_pat_<64 lowercase hex characters>
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_context","arguments":{}}}

The reply is HTTP 200 with Content-Type: application/json and a JSON-RPC response whose result has the tool result shape shown in §3.2.

initialize result:

{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"taskoh","version":"1.0"},"instructions":"Call get_context once before resolving project, member or label names. Tasks live in projects; statuses are inbox, planned, in_progress, in_review, needs_client, done. Use get_my_tasks for the user's own open work and search_tasks for everything else. Writes act as the signed-in user and appear in TaskOH's activity log. Full reference: https://api.taskoh.app/llms.txt"}

protocolVersion echoes params.protocolVersion if it is in the supported list, otherwise 2025-11-25.

3.2 Tool result shape

  • Success: {"content":[{"type":"text","text":"<JSON string of the result>"}],"structuredContent":<the same result object>}. Read structuredContent.
  • Business or permission failure: {"content":[{"type":"text","text":"<message>"}],"isError":true}. The message is meant for you; correct the arguments and retry.
  • Argument or schema failure: JSON-RPC error -32602 with the message in error.message.

4. Data model reference

4.1 Compact task (list rows)

Returned by get_my_tasks, search_tasks, and as subtasks in get_task. Key order is fixed.

{"id":42,"project_id":3,"project_name":"Acme Portal","title":"Login fails on Safari","status":"in_progress","type":"bug","priority":3,"due_date":"2026-09-12","assignee_name":"Maya Okafor","label_names":["frontend"],"subtask_count":2,"parent_id":null,"client_visible":true,"url":"https://app.taskoh.app/projects/3/tasks/42"}
KeyType
idinteger
project_idinteger
project_namestring or null (null if the project is not in your project list)
titlestring
statusstatus enum
typetype enum
priorityinteger 1–3 or null
due_dateYYYY-MM-DD string or null
assignee_namestring or null
label_namesstring[]
subtask_countinteger; omitted for guests (§4.5)
parent_idinteger or null
client_visibleboolean
urlstring, https://app.taskoh.app/projects/{project_id}/tasks/{id}

4.2 Full task

Returned by get_task, create_task, update_task.

KeyTypeNotes
idinteger
project_idinteger
parent_idinteger or nullnull = top-level task
titlestring
descriptionstring or null
detailsstring or null
statusstatus enum
typetype enum
scopescope enum
is_billablebooleanonly true when scope is out_of_scope
required_minutesinteger or nullpositive multiple of 15; hidden from guests unless billing is shared
fee_amountdecimal string or nulle.g. "1500.00"; hidden from guests unless billing is shared
fee_currencystring or null3 uppercase letters; hidden from guests unless billing is shared
billing_client_visiblebooleanwhether the client may see minutes and fee
client_visiblebooleanwhether guests see the task at all
created_by_user_idinteger or null
assignee_user_idinteger or null
priorityinteger 1–3 or null
due_dateYYYY-MM-DD or null
metadataobject, or [] when emptyan empty metadata is serialised as [], not {}
positionintegerboard position
created_atISO 8601 string
updated_atISO 8601 string
label_idsinteger[]
subtask_countintegeromitted for guests (§4.5)
assignee_namestring or null
assignee_emailstring or nullomitted for guests (§4.5)
assignee_last_activity_atISO 8601 string or nullomitted for guests (§4.5)
label_namesstring[]
project_namestring or null
urlstring

Scope and billing rules (enforced by update_task; each violation is an isError whose text is listed in §11):

  • is_billable: true requires scope: out_of_scope.
  • required_minutes must be a positive multiple of 15 (at most 4294967295).
  • fee_amount is a positive decimal string, integer part 1–10 digits without leading zeros, at most 2 decimals; the server stores and returns it normalised to two decimals ("1500" becomes "1500.00").
  • fee_amount requires fee_currency (^[A-Z]{3}$).
  • fee_currency without fee_amount is rejected.
  • minutes and fee must be null when not billable.

4.3 Project, member, label (from get_context)

{"me":{"id":7,"name":"Sam Rivera","email":"me@example.com"},
 "statuses":["inbox","planned","in_progress","in_review","needs_client","done"],
 "types":["request","bug","task"],
 "scopes":["unclassified","in_scope","out_of_scope"],
 "projects":[{"id":3,"name":"Acme Portal","my_role":"admin","client_portal_slug":"acme"}],
 "members":[{"id":7,"name":"Sam Rivera","email":"me@example.com","role":"admin"}],
 "labels":[{"id":11,"name":"frontend","color":"#f00"}]}
  • projects[]: id integer, name string, my_role one of admin, project_manager, guest, client_portal_slug string or null.
  • members[] and labels[] are present only when project was passed. members[].name may be null. When the caller is a guest in that project, members[] entries carry no email key at all (the caller's own me.email is still present).
  • me.name and me.email are string or null; fall back to the id when null.

4.4 Comment and activity shapes (from get_task)

{"comments":[{"id":5,"author_name":"Maya Okafor","body":"Looks good","created_at":"2026-09-06T14:10:00+00:00"}],
 "activity":[{"type":"task_created","type_label":"Task created","actor_name":"Sam Rivera","via":"api","created_at":"2026-09-06T14:10:00+00:00","metadata":{"title":"Login fails on Safari","via":"api"}}]}
  • author_name: the author's name, or their email when no name is set.
  • via is "api" for token-made events, otherwise null.
  • Activity type values: task_created, status_changed, approval_requested, approved, changes_requested, comment_added, attachment_added, visibility_changed, quote_sent, quote_approved, quote_rejected, scope_changed, billing_changed, billing_visibility_changed, task_deleted.

4.5 What guests see

AspectGuest behaviour
Task lists and get_taskonly tasks with client_visible: true; hidden tasks return Task <id> not found
Subtasks in get_taskonly client-visible subtasks
required_minutes, fee_amount, fee_currencyremoved unless the task is both client_visible and billing_client_visible
Activity in get_taskscope_changed, billing_changed, billing_visibility_changed events removed unless billing_client_visible
create_taskallowed, following the client portal's request rules: client_visible forced to true; type defaults to request; status, priority and due_date are ignored (the task starts in inbox without priority or due date); labels rejected with Only the project team can set labels; parent_task_id must be a client-visible task (a hidden, foreign or missing parent answers Task <id> not found); refused with Client requests are disabled for this project when the project does not accept client requests; the project team is notified of a new client request
update_task, delete_taskPermission denied: edit_task
add_commentallowed on client-visible tasks
get_context membersmembers[] entries carry no email key; the caller's own me.email is still present
Assignee errorscandidates listed as <name> (id <n>), without e-mail addresses
Assignee resolutionan e-mail address is not matched for a guest (use me, a member id or a name), so a guess cannot be confirmed
Task shapessubtask_count, assignee_email and assignee_last_activity_at are absent for guests (team members keep them)
parent_task_id in create_taskmust be a client-visible task; a hidden, foreign or missing parent answers Task <id> not found, so hidden tasks cannot be probed through the parent field
Task shapessubtask_count (compact and full task), assignee_email and assignee_last_activity_at (full task) are omitted

5. Tool reference

tools/list always returns all 8 tools in this order, regardless of role. Annotation sets:

SetValue
READ_ONLY{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
WRITE{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}
WRITE_IDEMPOTENT{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
DESTRUCTIVE{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false}

The schemas use one non-standard keyword, maxBytes: the maximum size of a string in UTF-8 bytes (maxLength counts characters). Both are enforced with a -32602 (§11).

Common isError texts (all tools that take a task or project):

TextMeaning
Task <id> not foundtask missing, in a project you are not a member of, or hidden from a guest — also for parent_task_id in create_task
No project named '<ref>'. Your projects: <name> (id <n>), ...unknown project (or Your projects: none)
Project name '<ref>' is ambiguous. Candidates: <name> (id <n>), ...two projects share the name; use the id

5.1 get_context

  • Title: Get workspace context. Annotations: READ_ONLY.
  • When: first call of every session. Also with project before any name-based create, update, or search.
  • inputSchema:
{"type":"object","additionalProperties":false,"properties":{"project":{"type":"string","description":"Project id or name. When set, also returns that project's members and labels."}}}
  • Returns: the object in §4.3. members and labels only when project is given.
  • Errors: -32602 for schema failures (unknown keys), isError for an unknown or ambiguous project — texts in §11.
  • Side effects: none. Permission: all roles.

5.2 get_my_tasks

  • Title: Get my open tasks. Annotations: READ_ONLY.
  • When: "what is on my plate", "my open tasks", "what is overdue for me".
  • inputSchema:
{"type":"object","additionalProperties":false,"properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"default":50}}}
  • Returns {"tasks":[<compact task>...],"total":2}. Order: overdue first, then priority descending, then due date ascending. Excludes done. Guests see only client-visible tasks. total is the number of rows returned. The limit is applied before role filtering, so a result shorter than limit does not prove there are no more tasks; to be exhaustive call again with limit: 100. Not paginated. Overdue = due_date earlier than the server's current date (UTC).
  • Errors: -32602 for schema failures (limit type and 1–100 bounds) — texts in §11.
  • Side effects: none. Permission: all roles.

5.3 search_tasks

  • Title: Search tasks in a project. Annotations: READ_ONLY.
  • When: any task listing that is not "my open tasks": by project, status, assignee, label, text, unassigned, done, subtasks.
  • inputSchema:
{"type":"object","additionalProperties":false,"required":["project"],"properties":{
 "project":{"type":"string","description":"Project id or name"},
 "query":{"type":"string","description":"Text in title or description"},
 "status":{"type":"array","items":{"type":"string","enum":["inbox","planned","in_progress","in_review","needs_client","done"]}},
 "type":{"type":"array","items":{"type":"string","enum":["request","bug","task"]}},
 "assignee":{"type":"string","description":"\"me\", \"unassigned\", user id, name or email"},
 "labels":{"type":"array","items":{"type":"string"},"description":"Label names; all must match"},
 "priority":{"type":"array","items":{"type":"integer","minimum":1,"maximum":3}},
 "scope":{"type":"array","items":{"type":"string","enum":["unclassified","in_scope","out_of_scope"]}},
 "is_billable":{"type":"boolean"},
 "parent_task_id":{"type":"integer","description":"Only subtasks of this task; 0 = top-level tasks only"},
 "sort":{"type":"string","enum":["position","priority","due_date","created_at","title","status"],"default":"position"},
 "order":{"type":"string","enum":["asc","desc"],"default":"asc"},
 "page":{"type":"integer","minimum":1,"default":1},
 "per_page":{"type":"integer","minimum":1,"maximum":100,"default":25}}}
  • Semantics: query matches title or description (substring, trimmed, empty ignored). status, type, priority, scope are OR within the array. labels is AND (all must be on the task). assignee: "unassigned" = no assignee. parent_task_id omitted = all tasks; 0 = top-level only; >0 = subtasks of that task. sort: "position" ignores order.
  • Returns {"tasks":[<compact task>...],"total":31,"page":2,"per_page":10,"total_pages":4}.
  • Errors: -32602 for schema failures (a missing project has its own message; array items report as status[0]), isError for an unknown or ambiguous project, assignee, or label — texts in §11.
  • Side effects: none. Permission: all roles; guests get only client-visible tasks.

5.4 get_task

  • Title: Get a task. Annotations: READ_ONLY.
  • When: the user asks about one task, before updating or deleting, or to read comments/activity.
  • inputSchema:
{"type":"object","additionalProperties":false,"required":["task_id"],"properties":{"task_id":{"type":"integer"},"include":{"type":"array","items":{"type":"string","enum":["comments","subtasks","activity"]},"default":["comments","subtasks"]}}}
  • Returns the full task (§4.2) plus, per include: comments (oldest first, max 50), subtasks (compact rows, position order, uncapped), activity (newest first, max 50). include: [] returns only the task.
  • Errors: -32602 for schema failures (task_id, include[n]), isError for task lookup — texts in §11.
  • Side effects: none. Permission: any member of the task's project; guest rules in §4.5.

5.5 create_task

  • Title: Create a task. Annotations: WRITE.
  • When: the user asks to add, create, log, or file a task, bug, or request, or to create a subtask.
  • inputSchema:
{"type":"object","additionalProperties":false,"required":["project","title"],"properties":{
 "project":{"type":"string","description":"Project id or name"},
 "title":{"type":"string","maxLength":255},
 "description":{"type":"string"},
 "type":{"type":"string","enum":["request","bug","task"],"default":"task"},
 "status":{"type":"string","enum":["inbox","planned","in_progress","in_review","needs_client","done"],"default":"inbox"},
 "priority":{"type":"integer","minimum":1,"maximum":3,"description":"1 low, 2 medium, 3 high"},
 "due_date":{"type":"string","format":"date","description":"YYYY-MM-DD"},
 "assignee":{"type":"string","description":"\"me\", user id, name or email; must be a project member"},
 "labels":{"type":"array","items":{"type":"string"},"description":"Existing label names; unknown names are rejected, never created. Project team only."},
 "parent_task_id":{"type":"integer","description":"Create as a subtask of this task (same project)"},
 "client_visible":{"type":"boolean","default":false}}}
  • Returns the full task (§4.2). Example (abbreviated):
{"id":42,"project_id":3,"parent_id":null,"title":"Login fails on Safari","description":null,"details":null,"status":"inbox","type":"bug","scope":"unclassified","is_billable":false,"required_minutes":null,"fee_amount":null,"fee_currency":null,"billing_client_visible":false,"client_visible":false,"created_by_user_id":7,"assignee_user_id":9,"priority":3,"due_date":"2026-09-12","metadata":[],"position":0,"created_at":"2026-09-06T14:10:00+00:00","updated_at":"2026-09-06T14:10:00+00:00","label_ids":[11],"subtask_count":0,"assignee_name":"Maya Okafor","assignee_email":"maya@example.com","assignee_last_activity_at":null,"label_names":["frontend"],"project_name":"Acme Portal","url":"https://app.taskoh.app/projects/3/tasks/42"}
  • Semantics: title is trimmed server-side; an empty or whitespace-only title (and the literal "0") is rejected with Title is required. due_date must be a valid calendar date in YYYY-MM-DD.
  • Errors: -32602 for schema failures (description must be a string; null is not accepted on create), isError for project/member/label lookup, guest label permission, title, due date, and parent-task failures — texts in §11.
  • Order of checks: project, labels, label permission, assignee, then the write. A bad name creates nothing.
  • Side effects: activity task_created (metadata.title, via: "api"). No assignment notification on create.
  • Permission: all roles. Guest: client_visible forced to true, type defaults to request, labels rejected.

5.6 update_task

  • Title: Update a task. Annotations: WRITE_IDEMPOTENT.
  • When: change status, assignee, due date, priority, title, description, labels, parent, visibility, scope, or billing of an existing task.
  • inputSchema:
{"type":"object","additionalProperties":false,"required":["task_id"],"properties":{
 "task_id":{"type":"integer"},
 "title":{"type":"string","maxLength":255},
 "description":{"type":["string","null"]},
 "details":{"type":["string","null"]},
 "status":{"type":"string","enum":["inbox","planned","in_progress","in_review","needs_client","done"]},
 "type":{"type":"string","enum":["request","bug","task"]},
 "priority":{"type":["integer","null"],"minimum":1,"maximum":3},
 "due_date":{"type":["string","null"],"format":"date"},
 "assignee":{"type":["string","null"],"description":"\"me\", id, name or email; null unassigns"},
 "parent_task_id":{"type":["integer","null"],"description":"null promotes to top level"},
 "add_labels":{"type":"array","items":{"type":"string"}},
 "remove_labels":{"type":"array","items":{"type":"string"}},
 "client_visible":{"type":"boolean"},
 "scope":{"type":"string","enum":["unclassified","in_scope","out_of_scope"]},
 "is_billable":{"type":"boolean","description":"Only allowed when scope is out_of_scope"},
 "required_minutes":{"type":["integer","null"],"description":"Multiple of 15"},
 "fee_amount":{"type":["string","null"],"description":"Decimal string, e.g. \"1500.00\"; requires fee_currency"},
 "fee_currency":{"type":["string","null"],"pattern":"^[A-Z]{3}$"},
 "billing_client_visible":{"type":"boolean"}}}
  • Only the given keys change. due_date: null clears the date; assignee: null unassigns; parent_task_id: null promotes to top level; priority: null clears priority. status and type must be valid enum values; an empty string is rejected with -32602. title is trimmed; an empty or whitespace-only title is rejected with Title is required (isError) — omit the key to leave the title unchanged. due_date must be a valid calendar date in YYYY-MM-DD (§5.5).
  • Returns the full task (§4.2) after the update.
  • Errors: -32602 for schema failures (labels is not accepted here; use add_labels/remove_labels), isError for task access and permission, member/label lookup, due date, parent (self, missing, circular), and the six billing rules in §4.2 — texts in §11.
  • Order of checks: task access, add_labels, remove_labels, then the write (which is where the edit_task permission is enforced — a guest with an unknown label sees the label error first). Unknown labels change nothing.
  • Side effects: activity status_changed {from,to}, scope_changed {from,to}, billing_changed, billing_visibility_changed only when the value actually changed. Assigning another user sends them an assignment notification (none when you assign yourself). No activity event for title, description, priority, due date, labels, parent, or client_visible changes (the visibility_changed event is emitted only by the web app's toggle).
  • Permission: admin and project_manager only.

5.7 add_comment

  • Title: Add a comment. Annotations: WRITE.
  • When: the user wants to leave a note, reply, or notify a project member on a task.
  • inputSchema:
{"type":"object","additionalProperties":false,"required":["task_id","body"],"properties":{"task_id":{"type":"integer"},"body":{"type":"string","minLength":1,"description":"Plain text. Mention a project member as @their.email@example.com to notify them."}}}
  • Returns:
{"id":77,"task_id":42,"author_name":"Sam Rivera","body":"Ship it @maya@example.com","created_at":"2026-09-06T14:10:00+00:00"}
  • author_name: the author's name, or their email when no name is set.
  • Errors: -32602 for schema failures (body must be a non-blank string), isError for task lookup — texts in §11.
  • Side effects: activity comment_added; every other project member gets a new-comment notification; each @email that belongs to a project member gets a mention notification (a mention of your own email is ignored). Mentions must be the full email, prefixed with @; names do not work.
  • Permission: all roles; guests only on client-visible tasks.

5.8 delete_task

  • Title: Delete a task. Annotations: DESTRUCTIVE (the only tool with destructiveHint: true).
  • When: only after the user has explicitly confirmed deletion of a specific task. Follow the steps in §7.8; rule in §8.
  • inputSchema:
{"type":"object","additionalProperties":false,"required":["task_id","confirm"],"properties":{"task_id":{"type":"integer"},"confirm":{"type":"boolean","const":true,"description":"Must be true. Deletes the task and all of its subtasks and comments permanently."}}}
  • Returns:
{"deleted_task_id":42,"deleted_subtask_ids":[43,44]}
  • Errors: -32602 for a missing, non-boolean, or false confirm (checked before any lookup), isError for task lookup and permission — texts in §11.
  • Side effects: the task, its subtasks, and its comments are removed permanently (no undo). Activity task_deleted with metadata {"task_id":42,"title":"...","subtask_count":2,"via":"api"} remains in the project log.
  • Permission: admin and project_manager only.

6. Name resolution rules

Always call get_context (with project when you will name members or labels) before relying on a name. Matching is exact and case-insensitive; there is no fuzzy matching.

ArgumentAccepted formsResolutionFailure
projectall-digit id (as a JSON string, e.g. "3") or project nameid matched first among your memberships; else case-insensitive exact nameNo project named '<ref>'. Your projects: ... / Project name '<ref>' is ambiguous. Candidates: ...
assigneeme, all-digit user id, email, nameme = token owner (no membership check); others among project members by id, email, or name. **Guests: id, name and me only** — an e-mail address never resolves for a guest, so it cannot be used to confirm who is a memberNo project member matching '<ref>'. Members: <name> <email> (id <n>), ... (guests: <name> (id <n>)) / Assignee '<ref>' is ambiguous. Candidates: ...
assignee in search_tasksalso unassignedtasks with no assigneeunassigned in create_task/update_task is looked up as a name and fails
labels, add_labels, remove_labelslabel namescase-insensitive exact name among the project's labels; duplicates collapsedUnknown label '<name>' in <project>. Available labels: <a>, <b>. Labels are not created automatically.
task_id, parent_task_idinteger id onlyTask <id> not found / Parent task not found in this project
  • Ambiguity: when a name matches more than one project or member, the error lists the candidates with ids. Use the id, or ask the user which one they mean.
  • Two members can share a display name; prefer email when the user gives one.
  • Labels are never created by the API. If a label does not exist, tell the user and proceed without it or ask.

7. Recipes

All calls are tools/call; only params is shown.

7.1 What is on my plate

{"name":"get_my_tasks","arguments":{}}

Report each task as title, project, status, due date, and url. Overdue tasks (as defined in §5.2) come first.

7.2 Create a bug in a project assigned to a person, due on a date

{"name":"get_context","arguments":{"project":"Acme Portal"}}

Confirm the project exists and that the person is in members (match name or email). Then:

{"name":"create_task","arguments":{"project":"3","title":"Login fails on Safari","type":"bug","priority":3,"assignee":"maya@example.com","due_date":"2026-09-12"}}

Report the returned id and url.

7.3 Move task N to done

{"name":"update_task","arguments":{"task_id":42,"status":"done"}}

If the reply is isError Permission denied: edit_task, the user is a guest in that project; say so.

7.4 Add a comment

{"name":"add_comment","arguments":{"task_id":42,"body":"Reproduced on Safari 18. @maya@example.com can you take a look?"}}

Use the member's email from get_context for mentions.

7.5 List unassigned high-priority tasks in a project

{"name":"search_tasks","arguments":{"project":"Acme Portal","assignee":"unassigned","priority":[3],"status":["inbox","planned","in_progress","in_review","needs_client"],"per_page":50}}

If total_pages > 1, repeat with page: 2 and so on.

7.6 Create a subtask

{"name":"create_task","arguments":{"project":"3","title":"Add Safari to the browser matrix","parent_task_id":42}}

The parent must be in the same project.

7.7 Mark a task out-of-scope, billable, with hours and fee

Hours must convert to a multiple of 15 minutes (2.5 h = 150). Send all fields in one call:

{"name":"update_task","arguments":{"task_id":42,"scope":"out_of_scope","is_billable":true,"required_minutes":150,"fee_amount":"1500.00","fee_currency":"EUR"}}

Add "billing_client_visible":true only if the user wants the client to see minutes and fee.

7.8 Delete a task

  1. get_task with include: ["subtasks"] to learn the title and subtask_count.
  2. Tell the user exactly what will be deleted: the title, the number of subtasks, and that comments are removed permanently.
  3. Wait for an explicit yes for that task. Do not treat the original request as consent.
  4. Only then:
{"name":"delete_task","arguments":{"task_id":42,"confirm":true}}
  1. Report deleted_task_id and deleted_subtask_ids.

8. Behaviour rules for agents

Do:

  • Call get_context once at the start of a session; call it again with project before resolving members or labels in that project.
  • After a name resolves, use ids (task_id, project id) for later calls.
  • Ask the user before delete_task; set confirm: true only after explicit consent for that specific task.
  • Report the task url after every create or update.
  • Paginate search_tasks with page when total_pages > 1; do not claim a list is complete otherwise.
  • On isError, read the message, correct the arguments (use the listed candidates, ids, or labels), and retry once. On -32602, fix the argument named in the message.
  • Prefer one update_task call with several fields over several calls.
  • On HTTP 429, wait the Retry-After period (60 s) before retrying and reduce call volume (fewer, larger calls: per_page up to 100, one update_task with several fields, get_context once per session); never retry in a tight loop. The token is unaffected (§2.2).
  • Tell the user when a result was filtered by their role (guest), when a permission error occurs, or when a project or member does not exist.
  • Send a descriptive User-Agent header (for example my-assistant/1.0) on every request. It is no longer required on the API host (§2.2), but it identifies your client in logs and keeps working if edge rules change.

Do not:

  • Invent, guess, or auto-create labels, members, or projects. Names must come from get_context.
  • Retry a write blindly after a -32603 Internal error or a transport failure; check with get_task or search_tasks first to see whether the write landed.
  • Expose, log, or repeat the token.
  • Assume a task exists because the user remembers it: Task <id> not found also covers tasks hidden from guests and tasks in projects the user is not a member of.
  • Use search_tasks without project; use get_my_tasks for the user's own work across projects.
  • Send "42" for an integer field or null where the schema does not list "null".
  • Send a JSON number for project — it is always a string, even when it is an id: "project":"3", not "project":3.

9. REST reference for GPT Actions

Schema: https://api.taskoh.app/openapi.json (OpenAPI 3.1.0, title TaskOH, version 1.0.0, server https://api.taskoh.app, security scheme bearerAuth). Import it with Authentication: API Key, Auth Type: Bearer.

operationIdMethod and pathisConsequentialInputSuccess
list_projectsGET /api/projectsfalsenone200 data: {count, projects[], workspace_role}
list_my_tasksGET /api/dashboard/my-tasksfalsequery limit (1–100, default 50; out-of-range values are clamped into that range, never a validation error)200 data: Task[]
list_project_tasksGET /api/projects/{id}/tasksfalsepath id; query search, status[], type[], assignee_user_id[], label_ids[], priority[], scope[], is_billable[] ("1"/"0"), parent_id, sort, order, page, per_page (1–100, default 50; out-of-range values are clamped into that range, never a validation error)200 data: {project_id, tasks[], total, page, per_page, total_pages}
create_taskPOST /api/projects/{id}/taskstruepath id; body TaskWrite, title required201 data: Task
get_taskGET /api/tasks/{id}falsepath id200 data: Task
update_taskPATCH /api/tasks/{id}truepath id; body TaskWrite200 data: Task
add_commentPOST /api/tasks/{taskId}/commentstruepath taskId; body {"body": "..."}201 data: Comment
  • Array query parameters use the [] suffix and are repeated for several values: ?status[]=inbox&status[]=planned.
  • list_project_tasks sort values: position, priority, due_date, created_at, title, status, type, scope, is_billable, assignee, client_visible.
  • TaskWrite fields: title (max 255), description, details (update only), status, type, priority (1–3 or null), due_date (YYYY-MM-DD), assignee_user_id, parent_id, client_visible, position (0–100000), scope, is_billable, required_minutes, fee_amount, fee_currency, billing_client_visible.
  • REST uses numeric ids only. Call list_projects first to map names to ids. Members and labels are not part of the REST operations; use MCP get_context (or ask the user) to obtain assignee_user_id and label_ids[].
  • The OpenAPI schema exposes no delete operation, so a GPT Action cannot delete tasks. Of the documented operations, only MCP delete_task deletes, and it requires confirm: true. Note that the token is not scoped to the documented operations: it acts as the user on the rest of the REST API as well (see the people guide, §9 and §10).
  • Success envelope: {"success":true,"data":...}. Error envelope: {"success":false,"error":{"type":"<type>","message":"<text>","code":<http status>}}, with details added on 422 validation errors. Types include unauthorized (401), forbidden (403), not_found (404), validation_error (422), and plan_limit_exceeded (422, when the workspace plan blocks the action; details.limit_type is max_projects or max_members_per_project and the message names the plan and limit).
  • Writes that produce activity events (see §1) are recorded in the activity log as "via API"; plain field edits are not logged.

10. Limits

LimitValue
search_tasks.per_pagemax 100, default 25
get_my_tasks.limitmax 100, default 50
get_task comments and activityfirst 50 comments, newest 50 events
get_task subtasksuncapped
REST list_my_tasks.limit and list_project_tasks.per_page1–100, default 50; out-of-range values are clamped (see §9)
title255 characters
Request execution timeLong-running requests are terminated server-side.
Result sizeno server limit; the LLM context window is the constraint. Use per_page and include to trim
Rate limiting120 requests per rolling 60-second window per token: one bucket per token (not per IP address), shared by every /api route and POST /mcp combined. A JSON-RPC batch costs one request per message (max 20) and is refused as a whole when it does not fit. Browser sessions calling POST /mcp share the same bucket, keyed per user; sessions on /api are not limited. Above the quota: HTTP 429 with Retry-After: 60 and the body {"success":false,"error":{"type":"rate_limited","message":"Too many requests. Please try again later.","code":429}} (on /mcp too a plain HTTP 429, not a JSON-RPC error). The token is not affected; wait 60 s, then retry with fewer, larger calls. Browser sessions are not limited by this rule (§2.2).
BatchMax 20 requests per batch (-32600 Batch too large (max 20) above that). Each message costs one request of the quota, and a batch that does not fit is refused as a whole with HTTP 429 without executing anything. Keep batches small (a handful of requests); batches are dispatched sequentially and are subject to the same execution limit as a single request.
Argument sizestitle 255 characters; description, details and body 65535 bytes; query 1000 bytes; page at most 100000; array filters at most 20 items (duplicates ignored). Over the limit: -32602 (§11).

11. Error catalogue

LayerCode / statusMessageAction
HTTP401Missing Authorization headersend the header
HTTP401Invalid Authorization header formatuse Bearer <token>
HTTP401Invalid or revoked tokenstop; user must create a new token at https://app.taskoh.app/integrations
HTTP401Invalid tokenbearer does not start with toh_; the token was pasted incompletely — resend the full toh_pat_… token
HTTP403This route requires an interactive sessionroute is token-blocked; not available to agents
HTTP403plain text error code: 1010, server: cloudflare, no JSONanswered by the CDN edge, not TaskOH (a zone security rule); should not occur on the API host since 2026-09-07 — retry after a short wait and report the cf-ray header if it persists; not an auth error
HTTP405empty body, Allow: POSTuse POST on /mcp; no SSE
HTTP429Too many requests. Please try again later. (Retry-After: 60)more than 120 requests in 60 s with this token; wait 60 s, retry, reduce volume
HTTP400JSON-RPC -32600 Malformed MCP-Protocol-Version headersend a YYYY-MM-DD value or omit the header
JSON-RPC-32700Parse errorbody is not valid JSON
JSON-RPC-32600Invalid Requestmessage is not an object, method missing, batch is [], or id is not a string, number or null
JSON-RPC-32601Method not foundonly initialize, ping, tools/list, tools/call exist
JSON-RPC-32602Missing tool nameset params.name
JSON-RPC-32602arguments must be an objectset params.arguments to an object
JSON-RPC-32602Unknown tool '<name>'. Available: get_context, get_my_tasks, search_tasks, get_task, create_task, update_task, add_comment, delete_taskfix the name
JSON-RPC-32602Missing required argument '<key>'add the key
JSON-RPC-32602Unknown argument '<key>' for <tool>remove the key
JSON-RPC-32602Argument '<path>' must be of type <t1> or <t2>fix the JSON type ("42" is not an integer)
JSON-RPC-32602Argument '<path>' must be one of: ...use an enum value; array items report as status[0]
JSON-RPC-32602Argument '<path>' must be truedelete_task.confirm
JSON-RPC-32602Argument '<path>' must be at least <n> / must be at most <n>integer bounds
JSON-RPC-32602Argument '<path>' must not be emptywhitespace-only string
JSON-RPC-32602Argument '<path>' must be at most 255 charactersshorten (title)
JSON-RPC-32602Argument '<path>' must be at most 65535 bytes / must be at most 1000 bytesdescription, details and body are capped at 65535 bytes, query at 1000 bytes; shorten
JSON-RPC-32602Argument '<path>' must have at most 20 itemsarray filters (status, type, labels, priority, scope, add_labels, remove_labels) take at most 20 items; duplicates are ignored
JSON-RPC-32602Argument 'page' must be at most 100000page is capped at 100000
JSON-RPC-32602Argument 'fee_currency' must match ^[A-Z]{3}$e.g. EUR
JSON-RPC-32602project is required — call get_context to list projects, or get_my_tasks for your own open work.add project to search_tasks
JSON-RPC-32603Internal errordo not retry writes blindly; verify state, then report
isErrorTask <id> not foundwrong id, foreign project, or hidden from guest
isErrorNo project named '<ref>'. Your projects: ...pick from the list
isErrorProject name '<ref>' is ambiguous. Candidates: ...use the id
isErrorNo project member matching '<ref>'. Members: ...pick from the list
isErrorAssignee '<ref>' is ambiguous. Candidates: ...use id or email
isErrorUnknown label '<name>' in <project>. Available labels: ... Labels are not created automatically.use an existing label or drop it
isErrorOnly the project team can set labelsguest; create without labels
isErrorPermission denied: edit_taskguest cannot update or delete
isErrorTitle is requiredempty or whitespace-only title on create or update (§5.5, §5.6)
isErrorInvalid due_date format. Use YYYY-MM-DDsend a valid calendar date in YYYY-MM-DD; see §5.5
isErrorParent task not found in this projectparent must be in the same project
isErrorA task cannot be its own parentchange parent_task_id
isErrorCannot set a descendant as the parent (circular reference)change parent_task_id
isErrorOnly out-of-scope tasks can be billed separatelyset scope: out_of_scope first or in the same call
isErrorRequired minutes must be a positive multiple of 15 (quarter-hour increments)round to 15
isErrorFee amount must be a positive decimal with at most 2 decimal placese.g. "1500.00"; integer part 1–10 digits, no leading zeros, at most 2 decimals (see §4.2)
isErrorFee amount must be greater than zeropositive amount
isErrorFee currency must be a 3-letter uppercase code when a fee is providedadd fee_currency
isErrorFee currency can only be provided with a fee amountadd fee_amount or drop the currency
isErrorClear the required hours and fee when a task is not billablesend required_minutes: null, fee_amount: null, fee_currency: null

12. Example JSON-RPC exchanges

12.1 initialize

Request:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}

Response (HTTP 200):

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"taskoh","version":"1.0"},"instructions":"Call get_context once before resolving project, member or label names. Tasks live in projects; statuses are inbox, planned, in_progress, in_review, needs_client, done. Use get_my_tasks for the user's own open work and search_tasks for everything else. Writes act as the signed-in user and appear in TaskOH's activity log. Full reference: https://api.taskoh.app/llms.txt"}}

The client's notifications/initialized message gets HTTP 202 with an empty body.

12.2 tools/list (abbreviated)

Request:

{"jsonrpc":"2.0","id":2,"method":"tools/list"}

Response:

{"jsonrpc":"2.0","id":2,"result":{"tools":[
 {"name":"get_context","title":"Get workspace context","description":"Returns the signed-in user, projects with your role, and the fixed status list. Pass a project to also get its members and labels.","inputSchema":{"type":"object","additionalProperties":false,"properties":{"project":{"type":"string","description":"Project id or name. When set, also returns that project's members and labels."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
 {"name":"delete_task","title":"Delete a task","description":"Deletes a task permanently, including its subtasks and comments. Requires confirm: true.","inputSchema":{"type":"object","additionalProperties":false,"required":["task_id","confirm"],"properties":{"task_id":{"type":"integer"},"confirm":{"type":"boolean","const":true,"description":"Must be true. Deletes the task and all of its subtasks and comments permanently."}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false}}
]}}

Only the first and last of the 8 entries are shown; the other six (get_my_tasks, search_tasks, get_task, create_task, update_task, add_comment, in that order between them) follow the same shape, and their exact inputSchema values are the ones printed in §5.2§5.7. There is no nextCursor; the list is complete.

12.3 create_task

Request:

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"create_task","arguments":{"project":"Acme Portal","title":"Login fails on Safari","type":"bug","priority":3,"assignee":"Maya Okafor","due_date":"2026-09-12","labels":["frontend"]}}}

Response:

{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\"id\":42,\"project_id\":3,\"parent_id\":null,\"title\":\"Login fails on Safari\",\"description\":null,\"details\":null,\"status\":\"inbox\",\"type\":\"bug\",\"scope\":\"unclassified\",\"is_billable\":false,\"required_minutes\":null,\"fee_amount\":null,\"fee_currency\":null,\"billing_client_visible\":false,\"client_visible\":false,\"created_by_user_id\":7,\"assignee_user_id\":9,\"priority\":3,\"due_date\":\"2026-09-12\",\"metadata\":[],\"position\":0,\"created_at\":\"2026-09-06T14:10:00+00:00\",\"updated_at\":\"2026-09-06T14:10:00+00:00\",\"label_ids\":[11],\"subtask_count\":0,\"assignee_name\":\"Maya Okafor\",\"assignee_email\":\"maya@example.com\",\"assignee_last_activity_at\":null,\"label_names\":[\"frontend\"],\"project_name\":\"Acme Portal\",\"url\":\"https://app.taskoh.app/projects/3/tasks/42\"}"}],"structuredContent":{"id":42,"project_id":3,"parent_id":null,"title":"Login fails on Safari","description":null,"details":null,"status":"inbox","type":"bug","scope":"unclassified","is_billable":false,"required_minutes":null,"fee_amount":null,"fee_currency":null,"billing_client_visible":false,"client_visible":false,"created_by_user_id":7,"assignee_user_id":9,"priority":3,"due_date":"2026-09-12","metadata":[],"position":0,"created_at":"2026-09-06T14:10:00+00:00","updated_at":"2026-09-06T14:10:00+00:00","label_ids":[11],"subtask_count":0,"assignee_name":"Maya Okafor","assignee_email":"maya@example.com","assignee_last_activity_at":null,"label_names":["frontend"],"project_name":"Acme Portal","url":"https://app.taskoh.app/projects/3/tasks/42"}}}

12.4 update_task

Request:

{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"update_task","arguments":{"task_id":42,"status":"in_progress","assignee":"me","add_labels":["backend"],"remove_labels":["frontend"]}}}

Response: same shape as §12.3 with status: "in_progress", assignee_user_id: 7, label_names: ["backend"], and a newer updated_at.

12.5 delete_task with confirm

Request:

{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"delete_task","arguments":{"task_id":42,"confirm":true}}}

Response:

{"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"{\"deleted_task_id\":42,\"deleted_subtask_ids\":[43,44]}"}],"structuredContent":{"deleted_task_id":42,"deleted_subtask_ids":[43,44]}}}

12.6 A -32602 error

Request (confirm omitted):

{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"delete_task","arguments":{"task_id":42}}}

Response (HTTP 200):

{"jsonrpc":"2.0","id":6,"error":{"code":-32602,"message":"Missing required argument 'confirm'"}}

Request (wrong type):

{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"get_task","arguments":{"task_id":"42"}}}

Response:

{"jsonrpc":"2.0","id":7,"error":{"code":-32602,"message":"Argument 'task_id' must be of type integer"}}

12.7 An isError result

Request:

{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"create_task","arguments":{"project":"Acme Portal","title":"Fix header","labels":["front-end"]}}}

Response (HTTP 200, JSON-RPC success, tool error):

{"jsonrpc":"2.0","id":8,"result":{"content":[{"type":"text","text":"Unknown label 'front-end' in Acme Portal. Available labels: frontend, backend. Labels are not created automatically."}],"isError":true}}

Correct call: "labels":["frontend"].

13. Changelog

VersionDateChange
1.52026-09-07Security hardening: a JSON-RPC batch of n messages costs n requests and is refused whole when it does not fit; browser sessions on POST /mcp share the per-user 120/60 s bucket; token-only endpoints now also cover member and team management, project deletion and Stripe routes; project payloads omit share-link fields for tokens; guest create_task follows the portal request rules (status/priority/due_date ignored, disabled requests refused, team notified); guests receive no member e-mails, subtask_count or assignee e-mails; validation caps for page, title, text fields and array filters; JSON-RPC id type check; the owner is e-mailed on token creation and holds at most 20 active tokens.
1.42026-09-07The CDN's browser-signature check was switched off for api.taskoh.app: Python's default Python-urllib/3.x User-Agent now works on the API host (taskoh.app and app.taskoh.app still apply the check). The error code: 1010 entries in §2.2 and §11 are kept as a diagnostic. No API change.
1.32026-09-07Per-token rate limiting introduced: 120 requests per rolling 60 s per token across /api and POST /mcp; HTTP 429 with Retry-After: 60 (batch charging and the session rule on /mcp arrived with 1.5).
1.22026-09-07Documented the CDN edge block: a request whose User-Agent is Python's default Python-urllib/3.x receives a plain-text 403 error code: 1010 from Cloudflare before reaching TaskOH. Send a descriptive User-Agent (§2.2, §8, §11). No API change.
1.12026-09-06title is trimmed server-side and an empty or whitespace-only title is rejected with Title is required on create and update. due_date must be a valid calendar date (calendar-invalid dates such as 2026-02-30 are rejected). REST list_my_tasks.limit and list_project_tasks.per_page are clamped to 1–100 (default 50) and openapi.json declares the same bounds.
1.02026-09-06First release. Authentication is by personal access token only (toh_pat_); OAuth is on the roadmap. 8 MCP tools, 7 REST operations, no token scopes.