18 routes
/todos
18 routes.
The to-dos themselves. A to-do is scheduled by whenDate (with an optional start whenTime, a length durationMin and up to 5 reminders), due by deadline, and those are different: a deadline alone does not put it on a day. A to-do can hold a checklist of subtasks.
- GET /v1/todos
- POST /v1/todos
- GET /v1/todos/{id}
- PATCH /v1/todos/{id}
- POST /v1/todos/{id}/complete
- POST /v1/todos/{id}/uncomplete
- POST /v1/todos/{id}/someday
- POST /v1/todos/{id}/anytime
- POST /v1/todos/{id}/move
- POST /v1/todos/{id}/trash
- POST /v1/todos/{id}/restore
- DELETE /v1/todos/{id}
- GET /v1/todos/{id}/notes
- POST /v1/todos/{id}/duplicate
- POST /v1/todos/{id}/subtasks
- PATCH /v1/todos/{id}/subtasks/{subtaskId}
- DELETE /v1/todos/{id}/subtasks/{subtaskId}
- POST /v1/todos/{id}/subtasks/{subtaskId}/promote
GET /v1/todos
List to-dos
In (sortKey, id) order. Trashed to-dos are excluded unless ?include_trashed=true. Headings: ?heading_id= keeps the to-dos under one heading (null = under none), and ?parent_id=<project>&group_by=heading returns the project’s to-dos in PAGE order (no heading first, then each heading’s, in heading order) with the project’s headings alongside, the way the apps draw the project page. headings (their names and open counts) is included only when the credential also holds containers:read; without it the to-dos still come in page order, each with its headingId. The cursor follows the heading order at the time of the request, so reordering headings between page requests can skip or repeat rows: page through quickly, or start over after a reorder.
Scopes: todos: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. |
parent_id |
string | optional | max 64 chars |
status |
open, someday, done |
optional | |
tag |
string or array of string | optional | max 64 chars, min 1 |
when_from |
string (yyyy-MM-dd) | optional | max 10 chars. A calendar date, yyyy-MM-dd. No time and no zone, so it is the same day everywhere. |
when_to |
string (yyyy-MM-dd) | optional | max 10 chars. A calendar date, yyyy-MM-dd. No time and no zone, so it is the same day everywhere. |
deadline_from |
string (yyyy-MM-dd) | optional | max 10 chars. A calendar date, yyyy-MM-dd. No time and no zone, so it is the same day everywhere. |
deadline_to |
string (yyyy-MM-dd) | optional | max 10 chars. A calendar date, yyyy-MM-dd. No time and no zone, so it is the same day everywhere. |
include_trashed |
true, false |
optional | |
group_by |
heading |
optional | heading: return the project’s to-dos in PAGE order (no heading first, then each heading’s, in heading order) and add the headings array. Needs parent_id. groupBy is accepted as an alias. |
groupBy |
heading |
optional | Alias of group_by. |
heading_id |
string | optional | max 64 chars. Only the to-dos under this heading, or the literal null for those under none. |
Response
- 200 TodoList. One page of to-dos.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * account_too_large * validation_failed * rate_limited * internal
POST /v1/todos
Create a to-do (unknown tags are created)
Headings: headingId puts the new to-do under one of its project’s headings; it must be a heading of parentId. Scheduling: whenTime (a start, HH:mm), durationMin (5 to 1440) and reminders (at most 5, each { "before": minutes } or { "at": "yyyy-MM-ddTHH:mm" }); a start with no whenDate dates the to-do today, as the apps do. subtasks creates the checklist. status: "someday" creates it parked, unless it also has a whenDate, a deadline, a recurrenceRule or a whenTime: a dated to-do is not parked, so it is created open. reminderTime and reminders cannot be sent together.
Scopes: todos:write Idempotent. Send Idempotency-Key and a retry replays the first answer instead of writing twice.
Request body
| field | type | notes | |
|---|---|---|---|
title |
string | required | max 512 chars, min 1 |
parentId |
string | optional | or null, max 64 chars |
headingId |
string | optional | or null, max 64 chars. A heading of the to-do’s project (/headings), or null for none. It must belong to the to-do’s parentId: a heading of another project, or a heading on a to-do with no project, is validation_failed at /headingId. |
subtitle |
string | optional | or null, max 512 chars |
description |
string | optional | or null, max 100000 chars |
tags |
array of string | optional | max 64 items |
whenDate |
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. |
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. |
status |
open, someday, done |
optional | |
priority |
low, medium, high |
optional | or null |
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. |
colorStyle |
text, fill |
optional | or null. Stored for compatibility. The apps draw a to-do’s colour as a highlight behind the row (fill) whatever this says. Omit it: a colorHex set without a style is written as fill, and clearing colorHex clears it. An explicit text is still stored. |
reminderTime |
string (yyyy-MM-ddTHH:mm) | optional | or null, max 16 chars. A LOCAL-NAIVE instant, yyyy-MM-ddTHH:mm: 24-hour, no seconds, no offset, no Z. It means that wall-clock time on the device that fires the reminder, so travelling never shifts it. |
recurrenceRule |
string | optional | or null, max 512 chars. The recurrence rule, as JSON: {"freq":"weekly","interval":1,"byday":[1,4]}. Validated by the same engine the apps run. Setting one clears deadline and anchors the series on whenDate; with no whenDate the rule's first day from today is stamped as both (today in afterCompletion mode). A rule whose until falls before that first day is refused here. Given to a parked to-do, the rule dates it the same way and takes it out of someday (status becomes open), so it lists on planned. null clears the rule and its anchor. |
sorted |
boolean | optional | Mark the to-do as sorted with no project (the "no project" answer). Only true is accepted: nothing clears it. A parent, a date or someday sort a to-do on their own. |
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. |
whenTime |
string | optional | or null, HH:mm, 24-hour. The start time on whenDate. On a to-do with no date (and no whenDate in this request) it dates the to-do TODAY (?today= or the server day), as the apps do. null removes the time: the start AND durationMin go, and every before reminder becomes a fixed at reminder at its old moment, so nothing the user set vanishes. |
durationMin |
integer | optional | or null, min 5, max 1440. The length in minutes, 5 to 1440. It may exist without a start (it is the block’s length once the to-do gets one). null clears it. |
reminders |
array of object | optional | or null, max 5 items. The WHOLE reminder list (at most 5), replacing the current one; null or [] clears it. Each is { "before": minutes } (needs a start) or { "at": "yyyy-MM-ddTHH:mm" }. Two that ring at the same moment are refused. reminderTime becomes the earliest of them. Do not send reminderTime in the same request. |
reminders[].before |
integer | optional | min 0, max 525600. Minutes before the start: 0 = at the start, 15, 60, 1440 = a day, 10080 = a week. Needs a start time (whenTime). |
reminders[].at |
string (yyyy-MM-ddTHH:mm) | optional | max 16 chars. A fixed local moment, yyyy-MM-ddTHH:mm (24-hour, no seconds, no offset). |
subtasks |
array of object | optional | or null, max 50 items. The WHOLE subtask list, in order (at most 50; together at most 10,000 bytes), replacing the current one. null or [] clears it. To change one subtask, use /todos/{id}/subtasks/{subtaskId}. |
subtasks[].id |
string | optional | max 64 chars. Keep an existing subtask’s id to keep its identity; omit it on a new one (the server issues a lowercase UUID). |
subtasks[].title |
string | required | max 480 chars, min 1. One line, at most 120 characters (counted as characters, not bytes); trimmed. |
subtasks[].done |
boolean | optional | Ticked. Default false. |
Response
- 201 Todo. The to-do as stored, with its tags resolved to names.
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/todos/{id}
One to-do
Scopes: todos:read
Response
- 200 Todo. The to-do.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * rate_limited * internal
PATCH /v1/todos/{id}
Change a to-do (not parentId or status, use the actions for those)
parentId and status are refused here: they are transitions with side effects, not field assignments. Completing a recurring to-do spawns the next occurrence, and parking one strips its dates. A parked to-do given a whenDate, a deadline, a recurrenceRule or a whenTime here comes back open in place, as in the apps, and lists on planned (and on today when the date is today): a recurring one restarts its series on its new date, a rule with no date stamps its first day, a time with no date dates it today. Clearing a date leaves it parked. A recurrenceRule clears deadline and anchors on whenDate, stamping the rule’s first day when there is none (see recurrence on devs.inittasks.com). Send null to clear any nullable field. headingId moves the to-do under another heading of its OWN project (or out of any, with null); to move it to another project use /move, which takes headingId too. Scheduling follows the apps: changing whenDate moves every before reminder with it; clearing it drops the start and every before reminder (fixed ones and the length stay); whenTime: null removes the start and the length and turns each before reminder into a fixed one at its old moment; reminders replaces the whole list; a lone reminderTime on a to-do that has a list replaces only the earliest reminder. subtasks replaces the whole checklist (keep an item’s id to keep it); for one subtask use /todos/{id}/subtasks/{subtaskId}.
Scopes: todos:write
Request body
| field | type | notes | |
|---|---|---|---|
title |
string | optional | max 512 chars, min 1 |
headingId |
string | optional | or null, which clears it, max 64 chars. A heading of the to-do’s project (/headings), or null for none. It must belong to the to-do’s parentId: a heading of another project, or a heading on a to-do with no project, is validation_failed at /headingId. |
subtitle |
string | optional | or null, which clears it, max 512 chars |
description |
string | optional | or null, which clears it, max 100000 chars |
tags |
array of string | optional | or null, which clears it, max 64 items |
whenDate |
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. |
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 |
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. |
colorStyle |
text, fill |
optional | or null, which clears it. Stored for compatibility. The apps draw a to-do’s colour as a highlight behind the row (fill) whatever this says. Omit it: a colorHex set without a style is written as fill, and clearing colorHex clears it. An explicit text is still stored. |
reminderTime |
string (yyyy-MM-ddTHH:mm) | optional | or null, which clears it, max 16 chars. A LOCAL-NAIVE instant, yyyy-MM-ddTHH:mm: 24-hour, no seconds, no offset, no Z. It means that wall-clock time on the device that fires the reminder, so travelling never shifts it. |
recurrenceRule |
string | optional | or null, which clears it, max 512 chars. The recurrence rule, as JSON: {"freq":"weekly","interval":1,"byday":[1,4]}. Validated by the same engine the apps run. Setting one clears deadline and anchors the series on whenDate; with no whenDate the rule's first day from today is stamped as both (today in afterCompletion mode). A rule whose until falls before that first day is refused here. Given to a parked to-do, the rule dates it the same way and takes it out of someday (status becomes open), so it lists on planned. null clears the rule and its anchor. |
sorted |
boolean | optional | Mark the to-do as sorted with no project (the "no project" answer). Only true is accepted: nothing clears it. A parent, a date or someday sort a to-do on their own. |
whenTime |
string | optional | or null, which clears it, HH:mm, 24-hour. The start time on whenDate. On a to-do with no date (and no whenDate in this request) it dates the to-do TODAY (?today= or the server day), as the apps do. null removes the time: the start AND durationMin go, and every before reminder becomes a fixed at reminder at its old moment, so nothing the user set vanishes. |
durationMin |
integer | optional | or null, which clears it, min 5, max 1440. The length in minutes, 5 to 1440. It may exist without a start (it is the block’s length once the to-do gets one). null clears it. |
reminders |
array of object | optional | or null, which clears it, max 5 items. The WHOLE reminder list (at most 5), replacing the current one; null or [] clears it. Each is { "before": minutes } (needs a start) or { "at": "yyyy-MM-ddTHH:mm" }. Two that ring at the same moment are refused. reminderTime becomes the earliest of them. Do not send reminderTime in the same request. |
reminders[].before |
integer | optional | min 0, max 525600. Minutes before the start: 0 = at the start, 15, 60, 1440 = a day, 10080 = a week. Needs a start time (whenTime). |
reminders[].at |
string (yyyy-MM-ddTHH:mm) | optional | max 16 chars. A fixed local moment, yyyy-MM-ddTHH:mm (24-hour, no seconds, no offset). |
subtasks |
array of object | optional | or null, which clears it, max 50 items. The WHOLE subtask list, in order (at most 50; together at most 10,000 bytes), replacing the current one. null or [] clears it. To change one subtask, use /todos/{id}/subtasks/{subtaskId}. |
subtasks[].id |
string | optional | max 64 chars. Keep an existing subtask’s id to keep its identity; omit it on a new one (the server issues a lowercase UUID). |
subtasks[].title |
string | required | max 480 chars, min 1. One line, at most 120 characters (counted as characters, not bytes); trimmed. |
subtasks[].done |
boolean | optional | Ticked. Default false. |
Response
- 200 Todo. The to-do 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/todos/{id}/complete
Complete (spawns the next occurrence when recurring)
Sets status: "done" and stamps completedAt. ⚠ A RECURRING to-do with a whenDate does not become done itself: the completion is recorded as a NEW done row (a copy of this one with no rule, which the completed view lists), and THIS row stays open and moves to its next occurrence (whenDate advanced, a count decremented). The response is this row, so for a recurring to-do it is the NEXT occurrence, still open. The series’ last occurrence (its count or until used up) completes in place like a plain to-do.
Scopes: todos:write
Response
- 200 Todo. The to-do: done, or for a recurring one its next occurrence.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal
POST /v1/todos/{id}/uncomplete
Un-complete
Scopes: todos:write
Response
- 200 Todo. The to-do, open again, with
completedAtcleared.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal
POST /v1/todos/{id}/someday
Park
Parking a to-do STRIPS its whenDate and deadline, because someday is timeline-free. A recurring to-do loses its dates too and keeps its rule, dormant: /move un-parks it with a new first date, and a PATCH with a whenDate un-parks it on that date. Any PATCH that gives a parked to-do a date, a deadline, a repeat rule or a start time takes it out of someday.
Scopes: todos:write
Response
- 200 Todo. The parked to-do.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal
POST /v1/todos/{id}/anytime
Detach and unschedule
No project, no whenDate, status: "open" (a done to-do re-opens and loses completedAt); deadline and tags stay. A parked RECURRING to-do comes back undated with its rule, as in the apps: with no date it has nothing to roll from, so completing it finishes it. Give it a whenDate and it rolls again.
Scopes: todos:write
Response
- 200 Todo. The to-do, open and undated.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal
POST /v1/todos/{id}/move
Re-parent and reorder
after/before name a to-do or a note in the destination list. Headings: headingId (a heading of the destination) files it under that heading; omitted, a move to ANOTHER container clears the heading (a heading belongs to one project) and a reorder in place keeps it.
Scopes: todos:write
Request body
| field | type | notes | |
|---|---|---|---|
parentId |
string | required | or null, max 64 chars |
headingId |
string | optional | or null, max 64 chars. A heading of the DESTINATION parentId, or null for none. Omitted: a move to another container clears the heading (a heading belongs to one project); a reorder in the same container keeps it. |
after |
string | optional | max 64 chars |
before |
string | optional | max 64 chars |
Response
- 200 Todo. The to-do 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/todos/{id}/trash
Trash a to-do
Scopes: todos:write
Response
- 200 Todo. The to-do, 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/todos/{id}/restore
Restore a to-do
Scopes: todos:write
Response
- 200 Todo. The to-do, 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/todos/{id}
Delete permanently
Irreversible. Its 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 to-do, trashed or not, and asks for no confirmation: trash it first (/trash) if you want a way back.
Scopes: todos:write
Response
- 204 No body. Deleted. Irreversible.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * rate_limited * internal
GET /v1/todos/{id}/notes
The notes linked to a to-do
Live notes linked to the to-do, pinned first, then editedAt descending (the to-do page’s notes section).
Scopes: notes:read, todos:read
Query
| parameter | type | notes | |
|---|---|---|---|
limit |
integer | optional | min 1, max 200 |
cursor |
string | optional | max 4096 chars |
Response
- 200 NoteList. One page of notes.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * validation_failed * rate_limited * internal
POST /v1/todos/{id}/duplicate
Duplicate a to-do
The apps’ duplicate: a new OPEN to-do right after this one, in the same project and heading, with every content field copied (dates, time, length, reminders, tags, priority, colour, repeat rule). Its subtasks are copied UNTICKED with new ids. A done to-do’s copy is open; a trashed to-do can be duplicated and its copy is live. The body is optional: { "id": … } chooses the new id.
Scopes: todos:write Idempotent. Send Idempotency-Key and a retry replays the first answer instead of writing twice.
Request body
| field | type | notes | |
|---|---|---|---|
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 Todo. The new to-do.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * conflict * duplicate_id * payload_too_large * unsupported_media_type * account_too_large * validation_failed * rate_limited * internal
POST /v1/todos/{id}/subtasks
Add a subtask
At the end of the checklist, or right after after. At most 50 per to-do; a title is one line of at most 120 characters.
Scopes: todos:write Idempotent. Send Idempotency-Key and a retry replays the first answer instead of writing twice.
Request body
| field | type | notes | |
|---|---|---|---|
title |
string | required | max 480 chars, min 1. One line, at most 120 characters; trimmed. |
after |
string | optional | max 64 chars. Insert right after this subtask; omitted = at the end. |
Response
- 201 SubtaskResult. The new subtask and the to-do with its whole list.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * conflict * payload_too_large * unsupported_media_type * account_too_large * validation_failed * rate_limited * internal
PATCH /v1/todos/{id}/subtasks/{subtaskId}
Rename, tick or move a subtask
Ticking the last open subtask does not complete the to-do. after moves it (an id, or null for first).
Scopes: todos:write
Request body
| field | type | notes | |
|---|---|---|---|
title |
string | optional | max 480 chars, min 1. The new title (one line, at most 120 characters). |
done |
boolean | optional | Tick or untick it. |
after |
string | optional | or null, which clears it, max 64 chars. Move it: right after this subtask, or null for first. |
Response
- 200 SubtaskResult. The subtask and the to-do with its whole list.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * payload_too_large * unsupported_media_type * account_too_large * validation_failed * rate_limited * internal
DELETE /v1/todos/{id}/subtasks/{subtaskId}
Delete a subtask
Scopes: todos:write
Response
- 200 Todo. The to-do without it.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * account_too_large * rate_limited * internal
POST /v1/todos/{id}/subtasks/{subtaskId}/promote
Make a subtask a to-do
The apps’ make task: a new open to-do titled as the subtask, right after its parent, in the same project, heading and day; the subtask leaves the list.
Scopes: todos:write Idempotent. Send Idempotency-Key and a retry replays the first answer instead of writing twice.
Response
- 201 SubtaskPromoted. The new to-do and the one it came from.
Errors: grant_expired * grant_revoked * unauthorized * forbidden_scope * not_found * conflict * payload_too_large * unsupported_media_type * account_too_large * rate_limited * internal