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.
Tokens
Section titled “Tokens”Create one in Settings → API tokens. Give it a name, and it is generated with:
- a
tdm_prefix; - least-privilege scopes, everything except the
:deletescopes; - a 90-day expiry.
The token list shows each token’s prefix, scope count, creation date, expiry and last use. Revoke takes effect immediately.
Scopes
Section titled “Scopes”| 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.
Using a token
Section titled “Using a token”curl -H "Authorization: Bearer tdm_..." \ "https://tasks.example.com/api/v1/tasks?completed=false"Endpoints
Section titled “Endpoints”| 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.
Start with /context
Section titled “Start with /context”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.
Listing tasks
Section titled “Listing tasks”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 |
1–4 |
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.
Delta sync
Section titled “Delta sync”Native clients pull changes rather than the whole workspace:
- Full pull once, keeping the
serverTimefrom the response. - Next pull:
?updatedSince=<that serverTime>&includeDeleted=true. - Keep the new
serverTimefor 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.
Idempotency
Section titled “Idempotency”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.
Device tokens
Section titled “Device tokens”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.
Errors
Section titled “Errors”| 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 |
OpenAPI
Section titled “OpenAPI”curl https://tasks.example.com/api/v1/openapi.jsonThe 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.
Health check
Section titled “Health check”GET /api/health needs no credential and reports database reachability:
200 {"status":"ok","db":"up"} or 503. Point your uptime monitor at it.
