Skip to content

REST API

Conatus serves a versioned REST API at /api/v1, described by an OpenAPI 3.1 document at /api/v1/openapi.json. It is the same API the MCP server and native clients use. There is no privileged back channel.

Create one in Settings → API tokens. Give it a name, and it is generated with:

  • a tdm_ prefix;
  • least-privilege scopes, everything except the :delete scopes;
  • a 90-day expiry.

The token list shows each token’s prefix, scope count, creation date, expiry and last use. Revoke takes effect immediately.

Scope Grants
tasks:read Read tasks, and the workspace context endpoint
tasks:write Create and update tasks
tasks:delete Move tasks to Trash
projects:read Read projects
projects:write Create and update projects and sections
projects:delete Move projects and sections to Trash
labels:read Read labels
labels:write Create and update labels
comments:read Read comments
comments:write Post and edit comments
comments:delete Delete comments
reminders:read Read reminders
reminders:write Create and update reminders
reminders:delete Delete reminders

A token missing the required scope gets 401, not 403.

Signed-in browser sessions carry an implicit * scope, which is why the same route handlers serve both the app and API clients.

curl -H "Authorization: Bearer tdm_..." \
"https://tasks.example.com/api/v1/tasks?completed=false"
Method and path Operation
GET /context Workspace context: user, timezone, today, Inbox, granted scopes
GET /projects List every accessible project
POST /projects Create a project
GET /projects/{id} One project
PATCH /projects/{id} Update a project
DELETE /projects/{id} Move a project to Trash
GET /sections?projectId= List sections
POST /sections Create a section
PATCH /sections/{id} Update a section
DELETE /sections/{id} Move a section to Trash
GET /tasks List tasks, cursor-paginated
POST /tasks Create a task
POST /tasks/quick-add Create a task from a natural-language line
GET /tasks/{id} One task with its labels, comments and reminders
PATCH /tasks/{id} Update a task
DELETE /tasks/{id} Move a task to Trash
GET /labels, POST /labels List and create labels
PATCH/DELETE /labels/{id} Update and delete a label
GET /comments, POST /comments List and post comments
PATCH/DELETE /comments/{id} Update and delete a comment
GET /reminders, POST /reminders List and create reminders
PATCH/DELETE /reminders/{id} Update and delete a reminder
POST /auth/device-token Exchange a password for a token. The only unauthenticated route

DELETE is always a soft delete. Nothing in the API permanently removes data.

curl -H "Authorization: Bearer tdm_..." https://tasks.example.com/api/v1/context
{
"apiVersion": "v1",
"serverTime": "2026-03-03T09:14:22.104Z",
"today": "2026-03-03",
"user": { "id": "…", "username": "…", "timezone": "Europe/Amsterdam", "dateFormat": "dd.MM.yyyy", "weekStart": 1 },
"inbox": { "id": "…", "name": "Inbox", "isInbox": true },
"grantedScopes": ["tasks:read", "tasks:write", "…"]
}

An agent should call this first. It resolves “today” in the user’s timezone, names the Inbox, and reports which scopes the token holds.

GET /tasks accepts:

Parameter Effect
projectId, sectionId, parentId Scope to a project, section or parent task
labelId Only tasks carrying that label. Must be a label you own
completed true or false
priority 14
dueBefore, dueAfter Inclusive YYYY-MM-DD bounds
query Case-insensitive substring over name and description
limit 1–100, default 50
cursor From the previous page’s nextCursor
updatedSince ISO datetime. See delta sync
includeDeleted true to include soft-deleted tasks

The response is:

{
"items": [ … ],
"nextCursor": "eyJ1cGRhdGVkQXQiOiI…",
"serverTime": "2026-03-03T09:14:22.104Z"
}

Results are ordered by updatedAt descending. The cursor is opaque. Pass it back verbatim; do not construct one. An invalid cursor returns 400.

Native clients pull changes rather than the whole workspace:

  1. Full pull once, keeping the serverTime from the response.
  2. Next pull: ?updatedSince=<that serverTime>&includeDeleted=true.
  3. Keep the new serverTime for the pull after that.

serverTime is read before the query runs, so a write landing mid-request falls inside the next sync window rather than between two of them.

includeDeleted=true matters: a soft-deleted task is the only record that a deletion happened, so a client that filters them out will never learn to drop the row.

Task creation accepts an Idempotency-Key header (1–200 characters):

curl -X POST https://tasks.example.com/api/v1/tasks \
-H "Authorization: Bearer tdm_..." \
-H "Idempotency-Key: 8f3c2b1a-…" \
-H "Content-Type: application/json" \
-d '{"projectId":"…","content":"Renew passport"}'

Behaviour:

Situation Response
First use of the key The request runs; status and body are recorded
Replay with the same body The recorded response, replayed
Replay with a different body 409, the key was used with a different request
Replay while the first is still running 409 with Retry-After: 1
Replay after 24 hours 409, the reservation expired. Use a new key

Keys are scoped per user and per operation, and retained for 24 hours. This is what lets an agent retry a flaky create without producing duplicate tasks.

POST /api/v1/auth/device-token is the one unauthenticated v1 route. A native client trades a username, password and device name for a token, so it never needs a browser session:

curl -X POST https://tasks.example.com/api/v1/auth/device-token \
-H "Content-Type: application/json" \
-d '{"username":"you","password":"…","deviceName":"Pixel 9"}'

The resulting token carries every agent scope and no expiry. A phone that stops syncing after 90 days reads as data loss, so revocation is the control rather than rotation. It appears in Settings → API tokens under the device name you gave, and is revoked from the same place.

The endpoint shares the login rate-limit budget: 5 attempts per username and 20 per IP address per 5 minutes, then 429 with a Retry-After header.

Status Meaning
400 Validation failed. The body carries per-field errors
401 No credential, an expired or revoked token, or a missing scope
404 Not found, or not yours. Conatus does not distinguish the two
409 Idempotency conflict
413 Attachment over the 10 MB limit
429 Rate limited. Honour Retry-After
curl https://tasks.example.com/api/v1/openapi.json

The servers entry is built from PUBLIC_BASE_URL, so set that if you generate clients against a deployed instance. The document is versioned with the application, not with this site.

GET /api/health needs no credential and reports database reachability: 200 {"status":"ok","db":"up"} or 503. Point your uptime monitor at it.