8 routes
/containers
8 routes.
The three nesting levels: an AREA sits at the top and holds projects and, like a project, its own to-dos, notes and attachments; a PROJECT sits in an area or at the top and holds subprojects, to-dos, notes and attachments; a SUBPROJECT sits in a project and holds to-dos, notes and attachments. "Container" is the one word for all three. A to-do with no container has no project. ⚠ GET /containers lists trashed containers too, with trashedAt set: filter on it.
- GET /v1/containers
- POST /v1/containers
- GET /v1/containers/{id}
- PATCH /v1/containers/{id}
- POST /v1/containers/{id}/move
- POST /v1/containers/{id}/trash
- POST /v1/containers/{id}/restore
- DELETE /v1/containers/{id}
GET /v1/containers
List containers
Areas, projects and subprojects, in (sortKey, id) order. Archived ones are included unless ?status=active. ⚠ TRASHED containers are listed too, with trashedAt set (there is no include_trashed switch here): filter on trashedAt if you want the live ones. A container under a trashed one is not itself stamped; the views hide the whole subtree.
Scopes: containers:read
Query
| parameter | type | notes | |
|---|---|---|---|
limit |
integer | optional | min 1, max 200 |
cursor |
string | optional | max 4096 chars |
updated_since |
string (RFC 3339) | optional | max 24 chars. An RFC 3339 instant, e.g. 2026-09-16T08:30:00.000Z. |
kind |
area, project, subproject |
optional | |
parent_id |
string | optional | max 64 chars |
status |
active, archived |
optional |
Response
- 200 ContainerList. One page of containers, oldest position first.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * account_too_large * validation_failed * rate_limited * internal
POST /v1/containers
Create a container
sortKey is issued at the end of the destination band. Supply id to make the create idempotent.
Scopes: containers:write Idempotent. Send Idempotency-Key and a retry replays the first answer instead of writing twice.
Request body
| field | type | notes | |
|---|---|---|---|
kind |
area, project, subproject |
required | |
name |
string | required | max 256 chars, min 1 |
parentId |
string | optional | or null, max 64 chars |
subtitle |
string | optional | or null, max 512 chars |
description |
string | optional | or null, max 100000 chars |
symbol |
string | optional | or null, max 64 chars |
colorHex |
string | optional | or null, 6-digit hex, with or without the #, max 16 chars. A 6-digit hex colour, with or without the leading #: #3A7BD5 or 3A7BD5. Three-digit shorthand is refused. |
deadline |
string (yyyy-MM-dd) | optional | or null, max 10 chars. A calendar date, yyyy-MM-dd. No time and no zone, so it is the same day everywhere. |
priority |
low, medium, high |
optional | or null |
id |
string (UPPERCASE UUID) | optional | max 64 chars. An uppercase UUID, all 36 characters, hex digits in capitals: 8E4F2A1B-7C3D-4E5F-9A0B-1C2D3E4F5A6B. Supplying one is how a create is made idempotent; a repeat is 409 duplicate_id. Lower case is refused, and so is a short id. |
Response
- 201 Container. The container as stored.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * conflict * duplicate_id * payload_too_large * unsupported_media_type * account_too_large * validation_failed * rate_limited * internal
GET /v1/containers/{id}
One container
Scopes: containers:read
Response
- 200 Container. The container.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * rate_limited * internal
PATCH /v1/containers/{id}
Change a container (not parentId or kind, use /move for those)
parentId and kind are refused here by design: re-parenting has to reissue a sortKey in the destination band, so it is /move. Send null to clear any nullable field.
Scopes: containers:write
Request body
| field | type | notes | |
|---|---|---|---|
name |
string | optional | max 256 chars, min 1 |
subtitle |
string | optional | or null, which clears it, max 512 chars |
description |
string | optional | or null, which clears it, max 100000 chars |
symbol |
string | optional | or null, which clears it, max 64 chars |
colorHex |
string | optional | or null, which clears it, 6-digit hex, with or without the #, max 16 chars. A 6-digit hex colour, with or without the leading #: #3A7BD5 or 3A7BD5. Three-digit shorthand is refused. |
deadline |
string (yyyy-MM-dd) | optional | or null, which clears it, max 10 chars. A calendar date, yyyy-MM-dd. No time and no zone, so it is the same day everywhere. |
priority |
low, medium, high |
optional | or null, which clears it |
status |
active, archived |
optional |
Response
- 200 Container. The container after the change.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * validation_failed * rate_limited * internal
POST /v1/containers/{id}/move
Re-parent and reorder
Moves exactly as a drag in the apps does, re-kinding where the new parent requires it: a project moved into a project becomes its subproject, and a subproject moved to an area or the top level becomes a project. Refused with validation_failed and the reason: anything into a subproject, an area anywhere but the top level, a container into its own contents, and a project that holds subprojects into a project (its subprojects would be nested too deep).
Scopes: containers:write
Request body
| field | type | notes | |
|---|---|---|---|
parentId |
string | required | or null, max 64 chars |
kind |
area, project, subproject |
optional | |
after |
string | optional | max 64 chars |
before |
string | optional | max 64 chars |
Response
- 200 Container. The container in its new position, with the reissued
sortKey.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * validation_failed * rate_limited * internal
POST /v1/containers/{id}/trash
Trash a container
Stamps trashedAt on the container and hides its descendants from every view. Reversible with /restore until the trash is emptied.
Scopes: containers:write
Response
- 200 Container. The container, now stamped
trashedAt.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal
POST /v1/containers/{id}/restore
Restore a container
Scopes: containers:write
Response
- 200 Container. The container, with
trashedAtcleared.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal
DELETE /v1/containers/{id}
Delete permanently (cascades)
Irreversible, and it takes every descendant container and to-do with it, and the notes whose every place is inside it (unless they are in a folder: any other note only loses this place). Attachments are released: one that is also somewhere else stays there, and one with no other place is deleted with its stored file. ⚠ It works on ANY container, trashed or not, and asks for no confirmation: trash it first (/trash) if you want a way back. The apps only offer it from the trash.
Scopes: containers:write
Response
- 204 No body. Deleted. Nothing is returned, and nothing comes back.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * rate_limited * internal