Skip to main content
Every REST error has the same shape. code is a stable machine name; message is English text for a person and may change.
details is present on validation errors only. It names every offending field, not only the first, with a dotted path for nested fields (repeat.every).

Codes every route can return

Business codes

An operation that refuses for a reason of its own says which, with a specific code: task_not_found, insufficient_role, not_archived, seat_limit. They use four statuses: The API never confirms that another account’s resource exists: that is a 404, not a 403.
94 codes, read from the API’s handlers. A ${…} in a message is shown as .

Behaviours worth knowing

Nothing is corrected silently. A priority of 9 is a 400, not a 4. An unknown top-level field in a JSON body is a 400: the input object is strict.
Unknown query parameters are ignored, so tracking parameters do not break a request. A misspelled one is therefore dropped rather than reported: check your spelling against the reference.
A body that is not valid JSON is read as no body at all. It fails with invalid_input when the operation requires a field, and is otherwise an empty input: a broken PATCH body is an empty patch.
The path wins. If a JSON body repeats a path parameter (PATCH /v2/tasks/{taskId} with a taskId in the body), the value from the path is used.
A 500 never leaks anything. Its message is always generic; the detail goes to the server’s logs. If you can reproduce one, it is a bug.

Batches are all or nothing

PATCH /v2/tasks, POST /v2/tasks/batch, POST /v2/tasks/complete, POST /v2/tasks/archive, PATCH /v2/tasks/schedule and POST /v2/hierarchy/reorder check every item before changing any. If one task is out of reach, the whole request fails and nothing changes.

Errors over MCP

The MCP server turns the same failures into tool results with isError: true and the message as text, so the model can read it and correct itself. See Protocol.