init.Tasks openapi.json inittasks.com

what a credential is allowed to touch

Scopes

A scope is one named permission on a credential. Every route needs one, every MCP tool needs the same ones as its REST route (the tools page lists them), and the server checks them on every request.

A write scope includes its read scope. todos:write gives you todos:read, so there is no write-only credential: a key that can add to the inbox can also read it. The apps show it the same way: ticking write ticks read, and read cannot be unticked while write is on.

The list

scope grants
todos:read todos:write to-dos
containers:read containers:write areas, projects, subprojects, and the headings inside projects
inbox:read inbox:write capture
tags:read tags:write tags
notes:read notes:write notes and everything on them: folders, places, links to to-dos and to other notes, tags on notes, version history and the export
attachments:read attachments:write attachments. attachments:read also reads file bytes
filters:read filters:write saved filters
settings:read settings:write week start, the two reminder settings, and the shared calendar choices
sessions:read sessions:write keys, grants and sessions
webhooks:write webhooks, including reading them
tasks:read tasks:write umbrellas over to-dos, containers, inbox, tags, notes, attachments, filters and settings, never sessions:* or webhooks:write

Rules that catch people out

The umbrellas do not imply sessions:* or webhooks:write. tasks:write is broad access to content. Listing a user's credentials, or wiring a channel that sends their activity to a URL, is a different kind of power, so it is asked for by name.

webhooks:write covers its own reads. Listing subscriptions reveals every endpoint the user has connected, so there is no read-only half.

attachments:write creates url and text attachments, uploads files and pictures, tags them, attaches them to more places and reorders them. attachments:read reads file bytes at GET /v1/attachments/{id}/file; inbox:read reads a capture's at GET /v1/inbox/{id}/file. Taking an attachment out of its LAST place makes an inbox item, so that one call needs inbox:write too.

GET /v1/export returns the whole account, so it needs every content read scope (todos, containers, notes, inbox, tags, attachments, filters) and settings:read. tasks:read covers all of them.

Three routes widen instead of refusing. GET /v1/search needs todos:read and containers:read, and then includes note hits only with notes:read, capture rows only with inbox:read and attachments only with attachments:read, because a hit carries their text. GET /v1/trash needs todos:read, and lists trashed notes and note folders only with notes:read, tags only with tags:read and filters only with filters:read. GET /v1/todos?group_by=heading includes the project's headings (their names) only with containers:read. So a credential scoped to to-dos still gets its trash and its grouped to-dos: asking what is in your trash should not fail because of a resource you never asked about.

Headings are containers:*. Reading, creating, renaming and reordering a heading needs containers:read or containers:write. Deleting one and making it a subproject also need todos:write, because they rewrite its to-dos; completing a heading's to-dos needs todos:write only.

DELETE /v1/trash needs notes:write as well when the trash holds a note or a note folder, tags:write when it holds a tag and filters:write when it holds a filter, because emptying destroys them.

A granted scope can change later

The user can edit a key's or a connected app's permissions from the app, at any time, without the credential itself changing. Your token keeps working; what it is allowed to do does not.

The credential's stored permissions are what the server checks:

The scopes sealed into your token when it was issued record the moment it was granted and are not consulted.

In practice: treat forbidden_scope as a live answer, not a startup check. A call that worked an hour ago can be refused now, and the fix is the same as it has always been: send the user through approval again, or ask them to tick the box.

GET /v1/me always reports the current list.

You may get less than you ask for

At approval time the user can grant a subset. Ask for todos:read todos:write and they may grant read only.

Read the granted scope from the token response, on the first token and on every refresh. Do not assume you got what you asked for.

json
{
  "access_token": "...",
  "token_type": "Bearer",
  "scope": "todos:read",
  "expires_in": 3600
}

GET /v1/me reports the same list at any time.

When a scope is missing

The route returns forbidden_scope. The credential is valid, so refreshing will not help. Send the user through approval again, asking for the scope you need.

Next