init.Tasks openapi.json inittasks.com

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:

text
area
  (to-dos, notes, attachments)
  project
    (to-dos, notes, attachments)
    subproject
      (to-dos, notes, attachments)

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.

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.

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:

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:

text
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", …}

⚠ 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:

bash
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.

bash
curl -s https://api.inittasks.com/v1/export \
  -H "Authorization: Bearer $INITTASKS_TOKEN" > inittasks-export.json

Every 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:

⚠ 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_.

Next