# Authentication

Every request carries a bearer token:

```http
Authorization: Bearer itk_7mQ2xLp9Va4Nk1Zr8Ts6Ye3Wu0Bd5Mg2Hj7Cq4Ln1Xv8
```

There 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](/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](https://www.rfc-editor.org/rfc/rfc7591) dynamic client registration, no review process, no waiting.

```bash
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**:

```text
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](https://www.rfc-editor.org/rfc/rfc8707)) 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:*` or `webhooks:write`.** `tasks:write` is 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:write` covers 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](/errors/unauthorized) and [grant_revoked](/errors/grant_revoked) by sending the user through approval again.
