Headless task store

Tasklet

Tasklet has no interface of its own. Point a client at the endpoints below and it keeps every device in step.

Endpoints

  • GET

    /api/todos

    List tasks, newest ordering first by position. Narrow with completed, includeDeleted, since, limit and offset.

  • POST

    /api/todos

    Create a task. Only title is required; notes, completed, position and dueAt are optional.

  • GET

    /api/todos/:id

    Read a single task. Add includeDeleted=1 to see one that was removed.

  • PATCH

    /api/todos/:id

    Update any subset of fields. Anything you leave out stays untouched.

  • DELETE

    /api/todos/:id

    Mark a task deleted so other clients learn about the removal. Add hard=1 to erase the record for good.

  • POST

    /api/todos/sync

    Two-way sync in one round trip: push local changes, pull everything that moved since your cursor.

  • GET

    /api/health

    Confirm the task store is reachable and see how many tasks are open.

Sync

Request
POST /api/todos/sync
Content-Type: application/json

{
  "since": "2026-09-17T08:00:00.000Z",
  "changes": [
    {
      "id": "8f0a2c1e-6b4d-4f2a-9c11-2d5f7ab30c41",
      "title": "Ship the sync endpoint",
      "completed": true,
      "updatedAt": "2026-09-17T08:04:11.920Z"
    },
    {
      "clientId": "draft-2",
      "title": "Write the client",
      "updatedAt": "2026-09-17T08:05:02.100Z"
    }
  ]
}
Response
{
  "syncedAt": "2026-09-17T08:05:02.100Z",
  "hasMore": false,
  "applied": [
    { "id": "8f0a2c1e-6b4d-4f2a-9c11-2d5f7ab30c41", "status": "updated" },
    { "id": "b41d77a0-1e93-4c88-bf20-6ac0f9e4d712", "clientId": "draft-2", "status": "created" }
  ],
  "changes": [
    {
      "id": "8f0a2c1e-6b4d-4f2a-9c11-2d5f7ab30c41",
      "title": "Ship the sync endpoint",
      "notes": null,
      "completed": true,
      "position": 0,
      "dueAt": null,
      "deletedAt": null,
      "createdAt": "2026-09-17T07:58:40.310Z",
      "updatedAt": "2026-09-17T08:04:11.920Z"
    }
  ]
}

How it behaves

Cursor
Store the syncedAt you get back and send it as since next time. When hasMore is true, call sync again straight away with the new cursor.
Conflicts
Last write wins, compared on updatedAt. If the stored copy is newer, your change is reported as skipped and the canonical task comes back in changes.
New tasks
Send an id you generated yourself to keep it stable offline, or leave id out and pass clientId so you can match the generated id in the applied list.
Deletions
Deletes are recorded, not erased: set deletedAt on a change, or call DELETE. Deleted tasks keep arriving in sync so every client can drop them.
Access
The endpoints are open until a TODO_API_KEY is set in the Secrets tab. Once it holds a real value, send it as an Authorization Bearer header or an X-Api-Key header.