tokens, the approval flow, scopes
Authentication
Every request carries a bearer token:
Authorization: Bearer itk_7mQ2xLp9Va4Nk1Zr8Ts6Ye3Wu0Bd5Mg2Hj7Cq4Ln1Xv8There is no API key, no client-credentials flow, and no way to act without a user. That is a consequence of the encryption design: a token that no user authorised would have no key, and a request with no key can read nothing. See encryption.
Personal access tokens
For your own scripts. Created in the app — Settings → account → encryption & devices → api keys — because only a device that already holds the encryption key can hand it to a new credential.
- Shown once. The server keeps no copy it can show you again.
- Expire after 30, 90, 180 or 365 days. Expiry is mandatory.
- At most 25 active at a time, at most 10 created per day.
- A token cannot create another token. Listing and revoking need
sessions:read/sessions:write; creating is first-party only.
OAuth 2.1
For an app other people will use. Registration is open — RFC 7591 dynamic client registration, no review process, no waiting.
curl -s https://api.inittasks.com/oauth/register \
-X POST -H "Content-Type: application/json" \
-d '{
"client_name": "Standup Bot",
"redirect_uris": ["https://standup.example/callback"],
"token_endpoint_auth_method": "none"
}'Then the standard flow, with PKCE (S256) required and resource mandatory:
GET https://api.inittasks.com/oauth/authorize
?response_type=code
&client_id=<your id>
&redirect_uri=https://standup.example/callback
&scope=todos:read todos:write
&resource=https://api.inittasks.com
&code_challenge=<S256 of your verifier>
&code_challenge_method=S256
&state=<your state>resource (RFC 8707) is not optional. A token is minted for one audience and refused at the other, so a token for the REST API cannot be replayed at the MCP server.
Approval happens on the user's device
There is no password form in your browser window. The user sees a nine-digit code, opens init.Tasks on a device they already trust, and approves there.
What they are shown, in this order: the callback address first, then your app's name as a subtitle. Names are self-declared and unverified — the host is the fact. There is no directory, no badge, and no review; identity is the address you registered.
They can also grant less than you asked for. If you request todos:read todos:write and they grant read, your token comes back with scope=todos:read. Read the granted scope from the token response; do not assume you got what you asked for.
Refresh tokens rotate
Every refresh returns a new refresh token and invalidates the old one, with a 30-second grace window for a response you did not receive.
⚠ Reuse of an already-rotated refresh token revokes the whole grant. That is the point: replay means the token leaked. Store the newest one atomically, and never run two refreshes concurrently.
A grant needs re-approval after 365 days.
Scopes
| scope | grants |
|---|---|
todos:read todos:write |
to-dos |
containers:read containers:write |
areas, projects, subprojects |
inbox:read inbox:write |
capture |
tags:read tags:write |
tags |
attachments:read attachments:write |
attachments, including file bytes |
filters:read filters:write |
saved filters |
settings:read settings:write |
week start |
sessions:read sessions:write |
keys, grants and sessions |
webhooks:write |
webhooks — including reading them |
tasks:read tasks:write |
umbrellas over the content scopes |
Two rules that surprise people, both deliberate:
- The umbrellas do not imply
sessions:*orwebhooks:write.tasks:writeis broad access to content. Enumerating a user's credentials, or wiring a channel that sends their activity to a URL, is a different kind of power and is asked for by name. webhooks:writecovers its own reads. Listing subscriptions reveals every endpoint the user has connected, so there is no read-only half.
A write scope implies its read scope: todos:write gives you todos:read.
Everything ends at once
Any of these disconnects every token, key, session and grant:
- [✓] the user changes their password
- [✓] the user changes their encryption key
- [✓] the user revokes your app in Settings
There is no partial state to recover from and no way to detect it in advance. Handle unauthorized and grant_revoked by sending the user through approval again.