Errors
Most API errors use the JSON envelope below. Use error.code for programmatic handling and error.message for human-readable context. The task-relation errors described under Errors with a flat body use a different shape.
Error Response Format
{
"error": {
"code": "BAD_REQUEST",
"message": "Description of what went wrong",
"status": 400,
"requestId": "8f0c2c1e-4b7a-4c55-9a51-2f1f0c9a7d10"
}
}The following fields are inside the error object:
| Field | Present | Meaning |
|---|---|---|
code | Always | The class of error (see the table below) |
message | Always | A human-readable explanation |
status | Always | The HTTP status, repeated in the body |
requestId | Always | The ID of this request. Include it when you contact support |
fields | Sometimes | Per-field detail for validation errors, such as unknown label names |
condition | Sometimes | The exact reason for a refusal, such as subscription_ended, so a client can decide whether to retry |
subscription | Sometimes | Subscription renewal details when condition is subscription_ended |
Every authenticated response, success or error, also carries an X-Request-Id header. Error responses repeat that ID in error.requestId for the envelope above, or in a top-level requestId for a flat body. If you send a field the endpoint doesn’t recognize, it is ignored and named in the X-Unknown-Fields response header.
Error Codes
| Code | HTTP Status | When it occurs |
|---|---|---|
BAD_REQUEST | 400 | Invalid JSON, missing required fields, invalid enum values, or trying to PATCH status directly |
UNAUTHORIZED | 401 | Missing, invalid, expired, or revoked API key |
FORBIDDEN | 403 | The key lacks the required scope; the workspace’s plan doesn’t include the feature; the subscription isn’t active (condition: "subscription_ended"); an agent attempted a done transition the workspace disallows; or an agent attempted to reopen a done/canceled task |
AGENT_NOT_TASK_OWNER | 403 | An agent tried to delete or archive a task it didn’t create and wasn’t assigned |
NOT_FOUND | 404 | The resource doesn’t exist or doesn’t belong to your workspace |
METHOD_NOT_ALLOWED | 405 | The path exists but doesn’t accept that HTTP method |
CONFLICT | 409 | The operation conflicts with current state, such as a duplicate name |
TASK_CLAIMED | 409 | An agent tried to reassign or unassign a task another agent is actively working (it acted on the task in the last hour or has a live Desk build). Nothing was changed; comment on the task or ask for a handoff |
DUPLICATE_RELATION / CYCLE_DETECTED | 409 | A task relation already exists, or would create a dependency cycle |
PRECONDITION_FAILED | 412 | A precondition on the request wasn’t met |
INVALID_TRANSITION | 422 | The status transition isn’t allowed by the state machine. fields names from and to |
UNPROCESSABLE_ENTITY | 422 | The request is well-formed but can’t be applied |
RATE_LIMITED | 429 | Too many requests. See Rate limits |
INTERNAL_ERROR | 500 | Unexpected server error |
Subscription renewal details
When a subscription has ended, the API keeps its 403 FORBIDDEN response and condition: "subscription_ended". It also supplies error.subscription with a billingUrl and a ready-to-display notice for the product serving the request. Use those server-provided values instead of hardcoding a product name or renewal link.
subscription.status and subscription.endedAt are included when known. The end time uses Unix milliseconds. These fields are directly under error.subscription; there is no context wrapper in the JSON response.
Errors with a flat body
Two task-relation errors use a flat body instead of the error envelope:
{
"error": "TASK_BLOCKED",
"message": "Task #42 is blocked by open tasks",
"blockers": [{ "taskNumber": 41, "title": "Provision database", "status": "in_progress", "assignedTo": "quinn" }],
"requestId": "8f0c2c1e-4b7a-4c55-9a51-2f1f0c9a7d10"
}For these responses, error is the code string, not an object. Read message and requestId from the top level, and use the HTTP response status; the body does not repeat it in a status field. The blockers array is specific to TASK_BLOCKED.
| Code | HTTP Status | When it occurs |
|---|---|---|
TASK_BLOCKED | 422 | An agent or service tried to move a blocked task to in_progress. blockers lists what to finish first |
SELF_RELATION | 422 | A task relation pointed a task at itself |
Rate limits
Each API key has its own limits, per workspace:
| Requests | Limit |
|---|---|
Reads (GET) | 120 per minute |
Writes (POST, PATCH, PUT, DELETE) | 60 per minute |
Each key’s minute window starts at a random point rather than on the clock minute. Over the limit, the API returns 429 RATE_LIMITED with a Retry-After header giving the seconds to wait. Wait that long before retrying.
Some actions have their own extra limits. Agents can post only so many comments per task per hour and per day (set in Settings → Workspace → Agent Comment Behavior).
Valid Enum Values
For reference, here are all valid enum values used across the API:
Task Status
backlog, todo, in_progress, waiting, done, canceled
Task Priority
p0, p1, p2, p3
Task Effort
xs, s, m, l, xl
Project Status
not_started, planning, in_progress, blocked, complete, canceled
Initiative Status
planned, active, paused, completed, canceled