containers, to-dos, the inbox, and what the views mean
The data model
The main kinds of row are containers, to-dos, headings, notes, capture rows, tags, attachments and saved filters. A note can also sit in a note folder, and a few small rows connect things: a note's places, its links to to-dos and to other notes, its versions, and an attachment's extra places. Everything else the API returns is a view over them, plus a short list of account settings and the sessions and devices that can reach them.
Containers
A container is an area, a project or a subproject. One table, one word, three values of kind.
The hierarchy is closed and three deep:
area
(to-dos, notes, attachments)
project
(to-dos, notes, attachments)
subproject
(to-dos, notes, attachments)- An area is always top level. It cannot be nested.
- A project sits under an area, or at the top level with no area.
- A subproject sits under a project. Nothing nests under a subproject.
The same rules apply when you create one: POST /v1/containers with a parentId that does not exist, is in the trash, or breaks the hierarchy (an area inside anything, a project under a project, a subproject with no project) is validation_failed at /parentId.
A move (POST /v1/containers/{id}/move) can change the kind, the way dragging does in the app. Move a project onto another project and it becomes a subproject of it. Move a subproject to the top and it is promoted to a project. Illegal moves are refused with the reason, including any attempt to move a container into its own contents, and moving a project that holds subprojects into another project: it would become a subproject with subprojects of its own, so it can go to an area or the top level only.
A container can be active or archived. Archiving hides it and its subtree from the views without trashing it. GET /v1/containers lists every container, archived and trashed ones included; read status and trashedAt.
To-dos
A to-do either has a parent container or it does not.
- Filed:
parentIdnames an area, a project or a subproject. - No project:
parentIdis null. These list first on every page, before the tree.
A to-do with no parent and no date is either still in the inbox or deliberately filed with no project; its sorted field says which. See lists, sorting and areas.
Inside a project or a subproject, a to-do can sit under a heading, a named group of the project's to-dos: headingId names it, and it is always a heading of the to-do's own parentId. Moving the to-do to another project clears it. See organizing a project with headings.
status is open, someday or done. There is no completed value.
A to-do scheduled on a day can also have a start (whenTime), a length (durationMin) and up to five reminders: see times, lengths and reminders. It can hold a checklist of subtasks: see subtasks and duplicates.
A to-do's colour, colorHex, is drawn as a highlight behind the row, never as coloured text (areas and projects keep coloured names). Set colorHex and leave colorStyle out: the API stores "fill", and clearing the colour clears the style. An explicit "text" is still stored, and the apps draw every coloured to-do as the highlight whatever it says.
Ordering inside a band comes from a stored sort key, not from a timestamp. Two rows can share a position only until the next write settles it.
Notes
A note is a page of Markdown you keep and read: a release history, a set of measurements, the paragraph you will want again in March.
A note is a sibling of a to-do, not a kind of one. It sits in the same ordered list inside a project, it moves to the same places, and it goes to the same trash. What it does not have is the rest of a to-do: no done state, no dates, no priority, no colour. It can carry tags (by name), which saved filters never match. So a note never appears in a date view and never counts toward a list or a container; the only numbers it moves are navCounts.notes and homeCounts.notes.
- Placed:
placeIdsnames the areas, projects and subprojects it is in, any number, oldest first;parentIdis the first. It cannot hang off a to-do: to-dos are linked to notes instead (many-to-many), and a to-do's own free text is itsdescription. - In no place:
placeIdsis empty andparentIdis null.
A note can also sit in a folder (folderId), be pinned, have a colour (colorHex), be linked to any number of to-dos (links) and of other notes (noteLinks), embed things in its text, and keep a version history. All of that, and where the apps show a note, is on notes, folders and links.
body is Markdown. The API stores and returns it exactly as you sent it: it does not render it and does not check the syntax. A note needs a title or a body: either may be empty, not both. Without a title the apps show the first line of the body.
A container's to-dos and its notes share one ordered list, an area's included. That is why after and before on POST /v1/notes/{id}/move may name either kind, and why containerItems in the overview payload mixes ids from both tables in one array. Resolve an id you find there against /v1/todos and /v1/notes.
note used to be a kind of attachment. It is not one now: POST /v1/attachments takes url and text. A note can HOLD attachments, though: "parentType": "note".
The inbox
The inbox is the doorway: everything caught and not sorted yet. It holds capture rows and unsorted to-dos, and GET /v1/views/inbox returns both. See REST and MCP for how each surface shows them.
An unsorted to-do is open, has no parent, has no whenDate, and has never been given an answer. A to-do leaves the inbox with one answer, and there are two kinds of answer:
- when: today, a date, anytime, or someday;
- where: an area, a project, a subproject, or no project.
Every answer sets sorted: true on the to-do in the same write, and nothing ever sets it back, so there is no "back to inbox". A to-do with no project and no date that has been answered is a normal to-do that lives in anytime. Tags, a deadline, a reminder, a priority, a title or a note are not answers: a to-do that only has those stays in the inbox. A reorder inside the same list is not an answer either.
One overlap is allowed: an unsorted to-do whose deadline has passed shows in the inbox and in today's overdue block, because a missed deadline must not hide.
A capture row is a scrap caught fast and sorted later: a line of text, a URL, a note you have not filed yet, or a file or picture someone shared. POST /v1/inbox captures text, a link or a note; POST /v1/inbox/upload captures a file or a picture as the share sheet does (see attachments). You can edit its text and its whenDate, reorder it, archive it, park it in someday, delete it, or move it onto a container.
What a move produces depends on the kind. A text capture becomes a sorted to-do, carrying its captured date, or a text attachment of the container when you send "as": "attachment". A note capture becomes a note, and needs notes:write as well. A url, image or file capture attaches to the target instead, and the attachment takes over its file. Send todoId instead of parentId to attach a capture to a to-do. The target container can be an area, a project or a subproject.
An image or file capture shared from a device that has not uploaded it yet reads pending: true. Moving it answers conflict, because its bytes are only on that device and only that device can file it. Try again after its upload lands, which arrives as an inbox.updated event. A capture that was already filed or deleted has no file left either: it reads pending: false, and moving it again is validation_failed. A link capture's stored icon moves to the attachment with it.
Moving with no parentId works for the first two: a text capture becomes a to-do with no project (sorted, so it lands in anytime), and a note capture becomes a standalone note. The other three have nothing to attach to without a target, so they are refused with validation_failed and a pointer at /parentId.
A note capture never becomes a to-do, with or without a target. It becomes a note.
Deleting a capture archives it. POST /v1/inbox/{id}/delete (MCP delete_inbox_item) is the apps' delete: the capture is archived, and a file or picture it holds is deleted from storage. POST /v1/inbox/{id}/archive archives it and keeps the file. Either way the row stays readable at ?status=archived. An image or file capture's bytes come from GET /v1/inbox/{id}/file.
What the views mean
Every open to-do is in exactly one of inbox, today, planned, anytime and someday.
| view | what it holds |
|---|---|
| inbox | unsorted to-dos and open capture rows |
| today | three blocks: overdue (a missed deadline), fromEarlier (scheduled on a day that has passed), todayItems (scheduled or due today); plus what was completed today |
| planned | scheduled after today: the next six days, one bucket each even when empty, then plannedLater with only the days that hold something |
| anytime | open, sorted to-dos with no date, whether or not they have a project, and not due today or earlier |
| someday | parked to-dos and parked capture rows |
all tasks (/v1/views/all-tasks, also /v1/views/overview) |
every open, sorted to-do by where: the ones with no project first, then the container tree with every live area, project and subproject in it, empty ones included; plus every count (navCounts) and the numbers the home screen shows (homeCounts) |
| notes | every note, in three blocks: still in the inbox, then by area or project, then the notes attached to nothing (in a folder or not). For folders and placement use GET /v1/notes (notes) |
| completed | done to-dos grouped by the local day they were completed, and each container's completed section (containerCompleted) |
Each is a route: GET /v1/views/inbox, /today, /planned, /anytime, /someday, /all-tasks (or /overview), /notes and /completed. upcoming and logbook are the old names of planned and completed. They still answer, with the new payload.
Every list puts the to-dos with no project first, then the tree in the order the apps show it: an area, the area's own to-dos, its projects, each project's to-dos, then its subprojects.
Three rules behind those that are not obvious:
A deadline alone does not schedule a to-do. whenDate puts a row in a date view. deadline does not. A sorted to-do with a deadline and no whenDate lists in anytime; on the deadline day it moves to today, and once the deadline has passed, to overdue. One still in the inbox stays there, and joins overdue once the deadline passes.
Parking strips dates. Moving a plain to-do to someday sets whenDate and deadline to null and keeps the parent, so a project idea parks with its project.
A date takes a to-do out of someday. Give a parked to-do a whenDate, a deadline, a recurrenceRule or a whenTime, and the same write makes it open in place: it lists on planned, or on today when the date is today. A rule with no date is dated on its first occurrence, and a start time with no date is dated today. A deadline alone does not date it, so it lists in anytime until the deadline day. Clearing a date leaves it parked. A POST with status: "someday" and any of those creates it open.
Areas hold to-dos, notes and attachments. An area may be the parent of any of them, and its count includes its own to-dos.
Planned always has six day buckets, tomorrow through six days out. It is not bounded by the calendar week and does not read the week-start setting.
Tags
A tag is identified by its name: its row id is derived from the normalised name (lower case, runs of whitespace collapsed). That is how two devices that create bug while offline end up with one tag.
Tags label to-dos, attachments and notes.
Renaming a tag moves it. PATCH /v1/tags/{id} with {"name": "defect"} gives the tag the id that defect derives to, rewrites every to-do, filter, attachment and note that used it, and answers with the tag at its NEW id. The old id stays behind as a forwarding pointer:
PATCH /v1/tags/tg_8c1e… {"name": "defect"}
→ 200 {"id": "tg_41d0…", "name": "defect", "colorHex": "#E5484D", …}
GET /v1/tags/tg_8c1e…
→ 308 Permanent Redirect
Location: /v1/tags/tg_41d0…
{"id": "tg_41d0…", "name": "defect", …}- A change of case only (
Bug→bug) is the same tag. Nothing changes and you get the tag back. - A renamed tag keeps its colour and its place in the list.
- Merging. If a tag called
defectalready exists, the rename is a merge, and it has to be asked for. Without"merge": trueyou get tag_exists, and nothing is written. With it, the tag's to-dos move todefect,defectkeeps its own colour, and a to-do that carried both ends up with one. - Old ids.
GET /v1/tagsnever lists an old id.GET /v1/tags/{oldId}answers308to the tag it became, with that tag as the body, so a client that does not follow redirects still gets it.PATCHandDELETEon an old id answer404: act on the tag at its new id. If you cache tag ids, refresh the cache from theLocationheader. - Creating an old name again gives you a fresh tag at that name: no colour, at the end of the list. The to-dos that were renamed stay where they went.
- Ordering.
PATCH /v1/tags/{id}withafter(a tag id, ornullfor first) orbeforemoves the tag in the list the apps show. - The trash. The apps'
deletemoves a tag to the trash:POST /v1/tags/{id}/trash, one write. Its to-dos, notes and attachments keep its name and filters keep their criteria, soPOST /v1/tags/{id}/restorebrings everything back as it was. While it is in the trash,GET /v1/tagsleaves it out (unlessinclude_trashed=true), the apps hide it from every list and pill, and a filter criterion that names it matches nothing (the apps mark the filter// a tag in this filter is in the trash * edit it). Creating a tag with its name restores it, with its colour and place. A tag in the trash is deleted for good after 30 days, like everything else there. The MCP'sdelete_tagtrashes too. - Deleting a tag (
DELETE /v1/tags/{id}, for good, live or in the trash) removes it from every to-do, attachment and note, and retires every old id that forwarded to it. It never changes what a filter matches: a criterion that names the tag is removed only where that makes no difference (is_notin an all-filter,isin an any-filter), and otherwise stays. A kept criterion reads back with the deleted tag's rawtg_…id, because the tag has no name any more, and a filter that required the tag now matches nothing. The apps mark such a filter// a tag in this filter was deleted * edit it.
⚠ Apps released before this change do not know about renamed tags yet. On those, a renamed tag's old name still shows in the tag list (with no to-dos), and creating that old name there does not bring it back. The apps catch up as each one is updated.
You pass tag names, not ids, when creating or patching a to-do, and a to-do's tags come back as names. Unknown tags are created, so there is no vocabulary to register first.
Attachments
An attachment is a link, a piece of text, a file or a picture. It can be in several places at once (to-dos, areas, projects, subprojects and notes, in any mix), it carries tags, and each place keeps its own order. Its first place is parentType / parentId; places lists them all. See attachments.
kind is url or text on create. A url row needs its url, an absolute http:// or https:// link (other schemes, such as javascript: or file:, are refused, here and on url captures); a text row needs its text. Each column belongs to its kind, on create and on PATCH alike. file and image rows are uploaded with POST /v1/attachments/upload (the raw file up to 100 MB, or base64 in JSON up to about 3 MB) or added in the apps. note was an attachment kind once and is not one now: the apps' note choice creates a note. See scopes.
A file can be up to 100 MB (100 MiB, 104,857,600 bytes). GET /v1/attachments/{id}/file returns the decrypted bytes of an image or file row; fileId is set exactly when it can.
Some rows are pending. An app can attach an image or file before its upload has finished, for example while offline. The row exists at once with pending: true and no fileId, and its bytes are still on the device that added it. /file answers not_found until that device uploads them, which arrives as an attachment.updated event. Treat a pending row as a file that is on its way.
A link can have its site's icon stored with it, which the apps show next to the link. The API does not serve the icon, and a link's fileId is always null.
Deleting an attachment deletes its stored file, including the file's preview image or the link's icon, from every place it is in. A permanent delete of a to-do, a container or a note takes only the attachments that have no other place. See trash.
Settings
GET /v1/settings returns the handful of preferences that belong to the account rather than to one device, so every client agrees about them:
| setting | what it is |
|---|---|
weekStart |
the first day of the week in the apps: the date picker's first column, and the day next week suggests. No view, bucket or count reads it |
reminderDefaultTime |
HH:mm, the time a reminder gets when it is switched on |
overdueNudgeTime |
HH:mm, one notification a day listing anything overdue; null means off |
calendarPrefs |
the shared calendar choices: show (events in today and planned) and, per calendar named <account>/<calendar>, its color and whether it is on |
PATCH touches only what you send. null is a value on the first three, never "leave it alone": weekStart: null is no preference, reminderDefaultTime: null restores the 09:00 default, and overdueNudgeTime: null turns the nudge off: the switch and the time are one value, so there is no "on at no time".
Two of them read back a default rather than a null, which is worth knowing before you diff a response: weekStart answers monday and reminderDefaultTime answers 09:00. overdueNudgeTime does read back null, because there it means off.
Whether a given device shows notifications at all is that device's own switch. It is stored on the device and has no API, because an account-wide switch would silence a phone from a laptop.
The calendars themselves live on each device: the apps read the device's own calendars, and nothing here connects or reads one. PATCH with calendarPrefs changes only the fields it names, and stamps each as the newest change, so devices that merge the prefs take yours for exactly what you touched. This switches one calendar off and leaves the rest alone:
curl -s https://api.inittasks.com/v1/settings -X PATCH \
-H "Authorization: Bearer $INITTASKS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"calendarPrefs": {"calendars": {"icloud/home": {"on": false}}}}'A calendar is keyed <account>/<calendar> as the device names it, lowercased by the server, and color is #RRGGBB or null for the calendar's own colour.
Each client also keeps an opaque blob of its own interface state. /v1/settings never exposes those; the export below carries them, as the apps' own export does.
Export
GET /v1/export returns the whole account as one JSON document, the inittasks-export-v1 document the apps put in their export ZIP: every container, to-do, heading, note, note folder, note link, capture, tag, attachment, filter and the settings, decrypted. It is the document only: each stored file is listed in files with path: null, and its bytes come from GET /v1/attachments/{id}/file or GET /v1/inbox/{id}/file. It needs every content read scope and settings:read.
curl -s https://api.inittasks.com/v1/export \
-H "Authorization: Bearer $INITTASKS_TOKEN" > inittasks-export.jsonEvery tag and every filter is in it, forwarding ones and the ones in the trash included; a tag or a filter in the trash carries trashedAt, and a live one has no such key.
The top-level keys are always format, version, exportedAt, userId, containers, todos, notes, inbox, tags, attachments, filters, settings and files. Seven more appear only when the account has any rows of their kind: headings after todos; noteFolders, noteLinks, notePlaces, noteNoteLinks and noteVersions after notes; and attachmentPlaces after attachments. A to-do's subtasks and a note's tags are left out the same way when empty. Treat a missing key as an empty list.
Devices and sessions
A session is one sign-in. A device is a client that has enrolled: it told the account its name and took a copy of the encryption key. Most sessions have a device; a session belonging to an access key or a connected app does not.
GET /v1/sessions joins the two, so one row can say what it is:
device: the enrolled device's name, platform andlastSeenAt, ornulldeviceType:phone,tablet,desktop,weborunknownlastActiveAt: the device'slastSeenAt, else the owning credential'slastUsedAt, elsenullgrant: the access key or connected app that owns this session, ornullfor a person's own sign-in
⚠ A device's name and platform are end-to-end encrypted, like everything else a user writes. A credential that holds the key reads them as text; the apps' own session, which holds no key on this side, reads null plus the envelope in name_encrypted / platform_encrypted and sees deviceType: "unknown". Never render an _encrypted field as text: it is v1.<nonce>.<ciphertext> and it will look like a name.
unknown is a real answer and not a failure to look: a session with no enrolled device genuinely is not a device.
Lengths
Text limits are counted in characters, the same number the apps stop typing at: a title is at most 512 characters, a description or a note's body 100,000. A character is a Unicode code point, which is how JSON Schema's maxLength counts: a letter or an emoji is one, an accented letter typed as a letter and a mark is two, a flag is two, and a family emoji joined from three people is five. The reference lists each field's limit; over it is validation_failed with a pointer at the field. A field that holds a code rather than text (an id, a sort key, a symbol name, a MIME type) is counted in UTF-8 bytes: plain ASCII is one byte a character, any other character two to four. A request body is at most 4 MB. A title or a name cannot be blank (spaces only count as blank).
Ids
Every row id is an uppercase UUID, for example 9C3F1A72-8E4F-4B21-A0D5-3F7B2C1E6A94. That is also the shape you must use if you supply your own id on a create, which is a second way to make a create safe to repeat. See errors and retries.
Two exceptions are derived rather than random: a tag's id is tg_ plus a keyed hash of its name (see tags), and the three built-in filters are fl_ plus a keyed hash of their slug.
Webhook subscriptions, events and deliveries use prefixed ids instead: wh_, evt_ and whd_.