DOEN

The DOEN API

Read your to-dos, add new ones and change them from your own scripts, Shortcuts and other apps. It is off until you turn it on, and you stay in control.

How it works

DOEN keeps your to-dos on your phone, and your phone stays the source of truth. When you turn on API access, DOEN uploads a copy of your open to-dos, and those finished in the last 30 days, to the DOEN server. The API reads that copy.

Reading is quick. Writing is not instant: a change you send waits on the server until your phone picks it up. DOEN does this while it is open, about every 20 seconds, and now and then in the background. If DOEN has been closed for a while, the change waits until you open it.

Every reply includes snapshot_at, the time the phone last uploaded. Use GET /status to see how fresh the data is and whether changes are still waiting.

Get started

  1. In DOEN, open Settings and choose API access.
  2. Choose whether the API may change your to-dos, or only read them. You can change this later.
  3. Tap Turn on API access. Copy your token and the base address.
  4. Try it from a terminal:
curl "https://doen.software/app/api/v1/tasks?list=today" \
  -H "Authorization: Bearer YOUR_TOKEN"

The base address for everything below is https://doen.software/app/api/v1. All requests and replies use JSON.

Authentication

Send your token in the Authorization header as a Bearer token. Treat it like a password. If it leaks, open Settings, API access and choose Get a new token, and the old one stops working at once.

Authorization: Bearer doen_0123456789abcdef0123456789abcdef01234567

Read to-dos

GET/tasks

Returns your to-dos. Everything is optional.

ParameterWhat it does
listopen (default, not completed), today, upcoming, anytime, future, log, or all. Each to-do belongs to one list, matching what DOEN shows.
completedtrue or false. Overrides the default of open to-dos only.
tagOnly to-dos with this tag.
projectOnly to-dos in this project, by name.
areaOnly to-dos in this area, by name. Includes to-dos in projects inside the area.
qSearch the title and notes.
updated_sinceAn ISO 8601 date and time, or a Unix timestamp. Only to-dos changed since then.
limit, offsetPaging. The default limit is 100 and the largest is 500.
{
  "ok": true,
  "snapshot_at": "2026-10-12T14:03:10Z",
  "total": 2,
  "limit": 100,
  "offset": 0,
  "tasks": [
    {
      "id": "6F0C2B1E-8A47-4D55-9B71-0C5E3A9D2F10",
      "title": "Call the plumber",
      "notes": "",
      "completed": false,
      "completed_at": null,
      "list": "today",
      "when": "today",
      "start": null,
      "deadline": "2026-10-13",
      "reminder": "2026-10-12T21:00:00Z",
      "tags": ["home"],
      "project": null,
      "area": "Home",
      "checklist": [],
      "created_at": "2026-10-10T09:12:44Z",
      "modified_at": "2026-10-12T08:30:01Z"
    }
  ],
  "pending_creates": []
}

pending_creates lists to-dos you created through the API that the phone has not collected yet, so they do not seem to go missing.

GET/tasks/{id}

Returns one to-do as task. If you created it moments ago and the phone has not collected it yet, you get "task": null and a pending entry instead.

Create a to-do

POST/tasks

Needs writing switched on. Only title is required.

curl -X POST "https://doen.software/app/api/v1/tasks" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Book flights",
    "notes": "Check the Tuesday fare",
    "when": "today",
    "deadline": "2026-10-20",
    "reminder": "2026-10-19T14:00:00Z",
    "tags": ["travel"],
    "checklist": ["Pick dates", {"title": "Compare fares", "done": false}]
  }'
{
  "ok": true,
  "id": "B7C1D5E2-3F4A-4B68-8C90-1A2B3C4D5E6F",
  "status": "queued",
  "message": "Queued. The to-do appears in DOEN the next time the phone syncs, ..."
}

The reply is 202 Accepted. The id is the id the to-do will have in DOEN, so you can use it straight away to change or delete it.

Change a to-do

PATCH/tasks/{id}

Needs writing switched on. Send only the fields you want to change. POST to the same address works too, for tools that cannot send PATCH.

FieldNotes
title, notesText. The title cannot be empty.
completedtrue moves the to-do to Log. false brings it back. Repeating to-dos create their next copy, just as when you tick them in the app.
whentoday, evening, anytime or future. Moving to Today or This Evening clears a future start date.
start, deadlineA date, or null to clear it.
reminderA date and time with a time zone, or null to clear it.
tagsThe full list of tags. It replaces the current tags.
project, areaA name, or null to remove. The name must already exist in DOEN, otherwise the change is refused. Setting a project removes the area, and the other way round.
checklistThe full checklist. It replaces the current one.
curl -X PATCH "https://doen.software/app/api/v1/tasks/6F0C2B1E-8A47-4D55-9B71-0C5E3A9D2F10" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'
{
  "ok": true,
  "command": { "id": "9C9CBA24-7D62-4BD6-BBE1-5FA668854388", "op": "update", "status": "queued" },
  "message": "Queued. The change is applied when the phone next syncs."
}

The reply is 202 Accepted. To find out whether the phone applied the change, ask GET /commands/{command id} (see Status). If the phone could not apply it, for example because the project name does not exist, the status is failed with a reason.

Delete a to-do

DELETE/tasks/{id}

Needs writing switched on. The to-do is deleted from DOEN when the phone next syncs. This cannot be undone, so check the id first.

Areas, projects and tags

GET/lists
{
  "ok": true,
  "snapshot_at": "2026-10-12T14:03:10Z",
  "areas": [ { "id": "...", "title": "Home" } ],
  "projects": [ { "id": "...", "title": "Kitchen", "area": "Home" } ],
  "tags": ["errands", "home", "travel"]
}

Status and change results

GET/status
{
  "ok": true,
  "snapshot_at": "2026-10-12T14:03:10Z",
  "snapshot_age_seconds": 42,
  "task_count": 187,
  "allow_write": true,
  "queued_changes": 0,
  "queued_creates": 0
}
GET/commands/{id}

Shows the outcome of a change you sent. status is queued, applied, failed or expired (the phone did not pick it up within 24 hours). Results are kept for 24 hours.

Backup and restore

These two endpoints read and write your full DOEN data (areas, projects, headings, tags, to-dos and checklists), not the to-do snapshot above. They need DOEN on the web switched on in the app, because they use the web copy. Restore also needs writing to be on.

GET/backup

Returns everything as JSON, in the same format as the Download backup button on the web. Add ?download=1 to get a file named doen-backup-ddmmyy.json. Limit: 30 requests per hour.

POST/restore

Send a backup file as the body. By default (mode=missing) it adds anything that is missing, brings back deleted items and leaves existing items alone. mode=overwrite replaces items that exist with the version in the file. Add dry_run=1 to see the counts without changing anything. Limit: 10 requests per hour, 8 MB per file.

{
  "ok": true,
  "dry_run": false,
  "mode": "missing",
  "added": 12,
  "brought_back": 3,
  "replaced": 0,
  "kept": 215,
  "skipped": 0,
  "total": 230
}

A body that is not a DOEN backup gets 400. 403 means writing is off, and 409 means DOEN on the web is off.

Automatic daily backup

# crontab -e  (every day at 02:30)
30 2 * * * curl -sf -H "Authorization: Bearer YOUR_TOKEN" \
  "https://doen.software/app/api/v1/backup" \
  -o "$HOME/doen-backups/doen-backup-$(date +\%d\%m\%y).json"

# restore, preview first
curl -s -X POST -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
  --data-binary @doen-backup-151026.json \
  "https://doen.software/app/api/v1/restore?dry_run=1"

MCP: connect Claude or another AI

DOEN is also a Model Context Protocol (MCP) server, so an AI assistant can read and add your to-dos. It uses the same token as the rest of the API and works on the copy kept for DOEN on the web, so DOEN on the web must be switched on.

POSThttps://doen.software/app/api/mcp.php
  1. In DOEN, turn on DOEN on the web, then Settings, API access. Choose whether the assistant may change your to-dos or only read them.
  2. Copy your token.
  3. In your assistant, add a custom connector or MCP server with the address above. Send the token as Authorization: Bearer YOUR_TOKEN.

Try it from a terminal:

curl -X POST "https://doen.software/app/api/mcp.php" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
ToolWhat it doesNeeds writing
list_todosList or search to-dos by list (inbox, today, upcoming, anytime, logbook, open, all), project or area.No
get_todoOne to-do with its notes and checklist.No
list_projectsProjects and areas.No
list_notesNote titles, or one note by id.No
list_habitsHabits, their days and the last seven days.No
add_todoAdd a to-do, with notes, a start date, deadline, reminder, project, area and checklist.Yes
update_todoChange a title, notes, schedule, deadline or reminder.Yes
complete_todoTick off a to-do, or reopen it.Yes
delete_todoDelete a to-do.Yes
check_habitMark a habit done for a day, or undo it.Yes

Pass utc_offset_minutes (for example -300) so "today" matches your time zone. Changes made through MCP reach your phone, Mac and browser the next time they sync. A tool that needs writing replies with an error if writing is off.

Anyone with your token can use these tools, so treat it like a password. Get a new token in DOEN to cut off an assistant at once.

Assistants that only run local servers

Some desktop apps can start a local program but cannot connect to a web address. For those, use mcp-proxy. It runs on your computer and passes everything on to DOEN. Install it with uv tool install mcp-proxy, then add DOEN to your assistant's MCP settings:

{
  "mcpServers": {
    "doen": {
      "command": "mcp-proxy",
      "args": [
        "--transport", "streamablehttp",
        "--headers", "Authorization", "Bearer YOUR_TOKEN",
        "https://doen.software/app/api/mcp.php"
      ]
    }
  }
}

If the app cannot find mcp-proxy, give the full path as the command, for example /Users/you/.local/bin/mcp-proxy. Restart the assistant or its connector after saving.

If it does not connect

To-do fields

FieldTypeMeaning
idstringA UUID. Stable for the life of the to-do.
title, notesstringThe text of the to-do.
completedbooleanWhether it is ticked off.
completed_atstring or nullWhen it was ticked off (UTC).
liststringWhere it appears: today, upcoming, anytime, future or log.
whenstringThe schedule you chose: today, evening, anytime or future.
start, deadlinedate or nullDates like 2026-10-20, in your local calendar.
reminderstring or nullDate and time in UTC.
tagslist of stringsTag names.
project, areastring or nullNames. A to-do in a project reports the project's area.
checklistlistItems like {"title": "Pick dates", "done": false}.
created_at, modified_atstringTimes in UTC.

Not included: to-dos made from calendar events, repeat rules and place reminders. They stay in DOEN.

Errors and limits

Errors look like {"ok": false, "message": "..."} with a matching HTTP status.

StatusMeaning
400Something in the request is not valid. The message says what.
401The token is missing, wrong, or was replaced or switched off.
403Writing is switched off in DOEN.
404No to-do with that id.
413The request is too large (64 KB at most).
429Too many requests, or too many changes waiting for the phone.

Examples

Python

import requests

BASE = "https://doen.software/app/api/v1"
HEADERS = {"Authorization": "Bearer YOUR_TOKEN"}

today = requests.get(f"{BASE}/tasks", params={"list": "today"}, headers=HEADERS).json()
for task in today["tasks"]:
    print(task["title"])

requests.post(f"{BASE}/tasks", headers=HEADERS,
              json={"title": "Water the plants", "when": "evening"})

JavaScript

const BASE = "https://doen.software/app/api/v1";
const headers = { Authorization: "Bearer YOUR_TOKEN", "Content-Type": "application/json" };

const res = await fetch(`${BASE}/tasks?tag=errands`, { headers });
const { tasks } = await res.json();

await fetch(`${BASE}/tasks/${tasks[0].id}`, {
  method: "PATCH", headers, body: JSON.stringify({ completed: true })
});

Shortcuts, Zapier and n8n

Use a "Get contents of URL" (Shortcuts) or HTTP request step. Set the method, add the header Authorization with the value Bearer YOUR_TOKEN, and send a JSON body for creating and changing. If you only need to add to-dos, the simpler email and webhook options in DOEN work too.

Privacy and security

Questions? Get in touch