Errors
The API uses conventional HTTP status codes: 2xx means your request succeeded, 4xx means something about the request needs to change, and 5xx means the failure was on our side.
Errors are RFC 9457 problem documents with a stable machine code and a traceId you can quote to support (also on the X-Trace-Id header):
{
"title": "Group task fields are derived",
"status": 400,
"detail": "Cannot set progress or status on a group task directly.",
"code": "parent_task_rollup",
"traceId": "6e0f…"
}
Match on
code, never on thetitletext or the HTTP status alone —titleis a human-readable summary and may change;codeis the stable contract.
Response codes
| Status | Meaning |
|---|---|
200 OK | Request succeeded. |
201 Created | A resource (task, dependency, resource) was created. |
204 No Content | Request succeeded; there’s nothing to return (deletes). |
400 Bad Request | The request body or parameters failed validation — see code. |
401 Unauthorized | Missing, wrong, or revoked token. |
403 Forbidden | Authenticated, but not allowed to do this — see code. |
404 Not Found | The resource doesn’t exist, or you can’t reach it (the API never distinguishes the two). |
409 Conflict | A concurrent write or a reused Idempotency-Key with a different body. |
429 Too Many Requests | Rate limit or daily quota exceeded — see Rate limits & quotas →. |
5xx | Something failed on our side. Safe to retry with backoff. |
Error codes
| Code | Status | Meaning |
|---|---|---|
forbidden | 403 | ReadOnly token attempting a write, or writing to a project you can’t write to |
project_read_only | 403 | the project is locked (lapsed / admin lock / pending deletion) — reason in detail |
user_agent_required | 403 | the request had no (or an empty) User-Agent header — see Authentication → |
missing_argument | 400 | a required field (e.g. name on create) was absent |
invalid_date / invalid_dates | 400 | unparseable date / end before start |
invalid_color | 400 | color must be #RRGGBB |
invalid_assignee | 400 | an assignee id isn’t a member or resource of the project |
invalid_parent | 400 | parent in another project, self-parent, or a hierarchy cycle |
parent_task_rollup | 400 | direct progress/status on a group task |
too_deep | 400 | delete would cascade through more than 20 levels |
too_many_items | 400 | a reorder batch over 1,000 items, or a dependency set over the per-task cap |
invalid_dependency / invalid_lag / invalid_dependency_type / cross_project / not_siblings / cycle_detected / graph_too_large | 400 | dependency graph rules |
conflict | 409 | concurrent modification lost — re-read and retry |
idempotency_conflict | 409 | an Idempotency-Key was reused with a different body |
rate_limited / quota_exceeded | 429 | see Rate limits & quotas → — honor Retry-After |
Two edge cases don’t carry a code: a malformed JSON body or wrong-typed field is rejected by the request binder as a bare 400 (validation problem document, no code), and anything you can’t reach is a plain 404 with no body detail — by design.
Idempotency and caching
POST endpoints that create things accept an Idempotency-Key header — replaying the same key and body returns the original response instead of creating a duplicate; the same key with a different body is 409 idempotency_conflict. Heavy GETs return a strong ETag; send it back as If-None-Match to get a cheap 304 Not Modified when nothing changed. See Rate limits & quotas → for both in full.
Handling errors
- Check the
codefield, not the HTTP status text — the same status (e.g.400) covers several distinct codes. - Retry
429s with backoff, honoring theRetry-Afterheader rather than a fixed delay. - Don’t retry
4xxother than429without changing the request — the request itself needs to change first. 5xxis always safe to retry with exponential backoff; if it persists, quote thetraceIdto support.