personal tokens, OAuth apps, and how approval works
Authentication
Every request carries a bearer token:
Authorization: Bearer itk_xPVI8n9JBtWHk76bLvrPdEWY5AuEdpkOCQkUVs-Whb6yThere is no API key, no client-credentials flow, and no way to act without a user. That follows from the encryption design: a token no user authorised would carry no key, and a request with no key can read nothing. See encryption.
Pick a personal access token for your own scripts, and OAuth for an app other people will use.
Personal access tokens
Create one in the app, under settings → api keys (the account page lists them too). A key is itk_ and 44 more characters. Behind it, the server keeps the key's sealed part: its sign-in and your encryption key, locked under a secret that exists only in the key you hold, so the server cannot open it without the key. Only a device that already holds the encryption key can hand it to a new credential. Keys made before 29 October 2026 are several hundred characters long and keep working until they expire.
Creating a key is first-party only. POST /v1/keys accepts the app's own session and no token of any kind, so there is no way to mint a key over the API, and no scope that grants it. Listing and revoking are different: they need sessions:read and sessions:write, and a token can hold those.
- The key is shown once. The server keeps no copy it can show you again.
- Expiry is mandatory: 30, 90, 180 or 365 days.
- The app may hold at most 25 active keys, and may create at most 10 a day. Both are limits on the account, reached through the app.
- Asking for a 26th returns conflict. Exceeding 10 in a day returns rate_limited.
- A key keeps working through a password change, and through a password reset. It ends when it expires, when it is revoked, or when the encryption key changes.
Changing a key's permissions
A key's scopes can be changed after it is created, without the key itself changing:
curl -s https://api.inittasks.com/v1/keys/$ID \
-X PATCH \
-H "Authorization: Bearer $APP_SESSION_JWT" \
-H "Content-Type: application/json" \
-d '{"scopes":["todos:read","todos:write","containers:read"]}'It is done from the app, under settings → api keys → a key → permissions, for the same reason creating one is: PATCH /v1/keys/{id} accepts the app's own session and no token of any kind. A leaked key cannot widen anything, its own scopes least of all.
The list replaces the whole set (it is a checklist, not a delta), and it may not be empty. To end a key, revoke it; a key with no scopes would still appear in the list as connected while answering forbidden_scope to everything.
PATCH /v1/grants/{id} is the same operation on a connected app, from settings → ai connections → an app → permissions. The app's existing access and refresh tokens keep working; what they are allowed to do changes underneath them.
The credential's stored row is what decides. On REST it is read on every request and cached for at most 60 seconds, so a change applies within a minute. A new MCP connection and a token refresh read it at once (the refreshed token's
scopeis the new list); an MCP connection that is already open keeps its tools until it reconnects. The scopes sealed into a token when it was issued are a record of that moment and nothing more.
⚠ There is no MCP tool for either of these. An AI connection must never be able to widen a key, or itself.
OAuth 2.1
Registration is open. It is RFC 7591 dynamic client registration, with no review and 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 authorization-code 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: a token for the REST API (https://api.inittasks.com, with or without a trailing slash) answers 401 at the MCP server, and a token for the MCP server (https://mcp.inittasks.com/mcp) answers 401 here.
scope takes any of the scopes, space-separated. Ask for sessions:read, sessions:write or webhooks:write by name if your app needs them: the umbrellas never include them, and on the approval screen each one starts switched off, so the user turns it on deliberately. A webhook your app creates is checked by the same URL guard as any other (webhooks).
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.
They are shown the callback address first, and your app's name below it. Names are self-declared and unverified, so 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. Read the granted scope from the token response, on every refresh too, rather than assuming. See scopes.
An account holds at most 25 connected apps. Approving a 26th is refused in the app with a plain sentence, and your waiting page keeps waiting until the user disconnects one and approves again.
When the user approves an app that is already connected (the same name and the same callback address), the app offers to replace the existing connection, switched on. Replacing ends the old connection, its access and refresh tokens, and the subscriptions it made, once the new one exists. So when your app connects again, expect the old tokens to stop working.
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.
Reusing an already-rotated refresh token revokes the whole grant. Replay means the token leaked, so the grant ends everywhere: the refresh tokens, every access token already issued (on the REST API and the MCP server alike), and the connection in the user's app list. Store the newest one atomically, and never run two refreshes at once.
A grant needs re-approval after 365 days.
When access ends
This disconnects every token, key, session and grant on the account, yours included:
- the user changes their encryption key
A password change does not end keys or connections. The user can sign their other devices out when they change it, and that never signs out a key or a connection. A password reset (the "forgot password" email) always signs the user's other devices out, and keys and connections keep working through it too.
And these end your app's grant, its access and refresh tokens, at once:
- the user revokes your app in the app, under settings → ai connections
- the user connects your app again and replaces the existing connection
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.
Next
- Scopes for what each permission covers.
- Errors and retries for what to do when a call fails.