docs/api-tokens.md
Rama main · réplica de lectura del repositorio original.
# API tokens
A Personal API token lets a script, a CLI, or an AI assistant work with your notes **as you**, without a browser session. This page covers the credentials — issuing them, what they reach, and how an app can authorize itself instead. What a program can then *do* with note content is documented separately in [AI editing](./ai-editing.md); the MCP endpoint that builds on all of it exposes up to twenty tools — seven that read, eight that write, three that move notes to the trash and back or delete folders, and two that issue short-lived transfer tokens: one for images, and one for reading and writing many notes as files (see [Which endpoints accept a token](#which-endpoints-accept-a-token), and [MCP](./mcp.md) for the tools themselves).
## What a token is
- A token acts **on your behalf**, within its scope. In this release it can list the notes you can read and the folders you can see, read a note's title and details, **read a note's content** as markdown (the whole note or one section, with the fingerprints a write needs — `GET /api/notes/:id/content`), and — with `notes:write` — **write a note's content**: create a note with content in it, replace the whole note, replace, delete or insert after one section, append to the end, and undo any of those again. Writes are guarded by those fingerprints, so a change made from a stale read is refused rather than overwriting what someone typed in the browser meanwhile; see [AI editing](./ai-editing.md). Through the MCP tool `create_transfer_token`, it can also issue a short-lived transfer token that lets a shell command upload an image to, or download the images of, one note it could already write or read. Through `open_notes_as_files`, it can also issue a short-lived token that lets shell commands download the notes it can read as files and, with `notes:write`, send changed files back to the notes it can edit. Through MCP only, it can also rename a note it can edit, move one of your personal notes into one of your groups (which removes its per-person shares, public link and version history), and copy a note it can read; moving, and copying into a group, also need `notes:move`. Also through MCP only, it can list the notes in your trash (`list_trash`), and — with `notes:delete` — move a note to the trash and bring one back (`delete_note`, `restore_note`), for the same notes as you can in the web app. And through MCP only, with `notes:write`, it can create, rename and move folders and file a note in a folder (`create_folder`, `rename_folder`, `move_folder`, `move_note_to_folder`), and — with `notes:delete` — delete a folder with the folders in it, which moves the notes in your own or a group's folders to the trash (`delete_folder`); creating a note in a folder doesn't need MCP (`POST /api/notes` takes a `folderId`). The REST endpoints for everything listed here as MCP only still accept only a signed-in browser session. It cannot do anything else (no managing a note's shares or public link directly, no password changes, no admin actions, no deleting notes permanently, no emptying the trash). See [Which endpoints accept a token](#which-endpoints-accept-a-token).
- Tokens start with `knb_` and are 47 characters long. The server stores only a SHA-256 hash; **the plaintext is shown exactly once**, when you create it. If you lose it, revoke it and create a new one.
- Tokens are **separate credentials from your password** — see [Security notes](#security-notes).
## Creating one
**Settings → Authorization → API tokens → Create API token.**
- **Name** — anything that helps you tell tokens apart later (e.g. the program that will use it). 1–64 characters.
- **Access** — reading (`notes:read`) is always included. Three switches add to it, and all three start off: *Create and edit notes* (`notes:write`) lets the token create notes, change the content of notes you already have, and revert those changes — see [AI editing](./ai-editing.md) — and, over MCP, create, rename and move folders and file notes in them; *Move or copy notes into groups* (`notes:move`) lets it, over MCP, move one of your personal notes into one of your groups and copy notes into your groups; *Move notes to the trash and restore them* (`notes:delete`) lets it, over MCP only, move notes to the trash and bring them back with `delete_note` and `restore_note`, and delete folders with `delete_folder`, which moves the notes in your own or a group's folders to the trash — it doesn't let it delete a note permanently. `notes:move` and `notes:delete` only come together with `notes:write`: their switches can't be turned on until *Create and edit notes* is, and turning *Create and edit notes* off turns them off too. Moving a note into a group can't be undone — it removes the note's per-person shares, public link and version history — and every member of the group can read what is moved or copied there; that warning is shown under the switches while *Move or copy notes into groups* is on. While *Move notes to the trash and restore them* is on, the text under it says that a note left in the trash can still be deleted permanently later, by the site's trash setting or by someone in the web app. The token table shows each token's access as tags listing only what it has: *Read* on every row, plus *Edit* (`notes:write`), *Move* (`notes:move`, in red) and *Delete* (`notes:delete`).
- **Expires** — never (the default), or in 30 / 90 / 365 days. A token with no expiry keeps working until you revoke it.
Copy the token from the dialog before closing it. It will not be shown again.
You can hold up to **20 active tokens** by default (a site admin can lower this limit; expired ones don't count). OAuth app credentials count toward the same limit — so with 20 authorized apps you cannot create a personal token until you revoke one. Creating tokens is rate-limited to 10 per hour per user. Transfer tokens don't count toward the limit.
## Using one
Send it as a Bearer token:
```sh
curl -H "Authorization: Bearer knb_…" https://<your-host>/api/notes
```
```sh
curl -X POST -H "Authorization: Bearer knb_…" -H "Content-Type: application/json" \
-d '{"title":"From a script"}' https://<your-host>/api/notes
```
Responses are the same JSON the browser UI gets. On these endpoints a request that carries an `Authorization` header is authenticated **only** by that header — a session cookie sent alongside it is ignored.
What you get back from `GET /api/notes/:ref` is note **metadata** — id, title, slug, owner, your role, timestamps, and who last edited it. The note's content is readable through `GET /api/notes/:id/content` (the whole note as markdown, or one section, together with the fingerprints a write needs) and writable through `POST /api/notes/:id/edits`. `POST /api/notes` can also carry a `content` field, so a note can be created with its text in a single request rather than starting empty:
```sh
curl -X POST -H "Authorization: Bearer knb_…" -H "Content-Type: application/json" \
-d '{"title":"From a script","content":"# Notes\n\nWritten by a program.\n"}' \
https://<your-host>/api/notes
```
⚠ `POST /api/notes` rejects **unknown** body fields with `400 invalid_body` rather than ignoring them — so a misspelled `content` is reported instead of quietly creating an empty note.
Everything a program needs in order to write safely — how sections are addressed, what the fingerprints mean, the five operations, the error codes and how to undo a change — is in [AI editing](./ai-editing.md).
### Which endpoints accept a token
| Endpoint | Scope needed |
|---|---|
| `GET /api/notes` — list your notes | `notes:read` |
| `GET /api/notes/:ref` — read one note's metadata | `notes:read` |
| `GET /api/notes/:id/content` — read a note's content as markdown (whole or one section, with fingerprints) | `notes:read` |
| `GET /api/notes/:id/edits` — list a note's recorded API writes, and whether each can still be undone | `notes:read` |
| `GET /api/folders` — list the folders you can see (in your notes, in notes shared with you, and in your groups) | `notes:read` |
| `GET /api/search` — search the titles and body text of the notes you can see | `notes:read` |
| `POST /api/notes` — create a note, optionally with its `content` and in a folder (`folderId`) | `notes:write` |
| `POST /api/notes/:id/edits` — write a note's content: replace the whole note, replace/insert after/delete one section, or append | `notes:write` |
| `POST /api/notes/:id/edits/:editId/revert` — undo one recorded write | `notes:write` |
| `POST /api/mcp` (`GET`/`DELETE` → `405`) — MCP endpoint | `notes:read` for `list_notes`/`search_notes`/`read_note_outline`/`read_note_section`/`read_note_image`/`list_trash`/`list_folders`; `notes:write` (which includes read) for `edit_note`/`create_note`/`copy_note`/`create_folder`/`rename_folder`/`move_folder`/`move_note_to_folder`; `notes:move` (which comes only with `notes:write`) for `move_note_to_group` and for `copy_note` with `group_id`; `create_transfer_token` needs `notes:read` for `download` and `notes:write` for `upload`; `open_notes_as_files` needs `notes:read`, and its token can send files back only while the credential has `notes:write`; `notes:delete` (which also comes only with `notes:write`) for `delete_note`/`restore_note`/`delete_folder` |
The two upload endpoints, `POST /api/notes/:id/uploads` and `GET /api/uploads/:id`, do **not** accept a token: they take a session cookie or a transfer token from `create_transfer_token`, and answer a token with `401` pointing at that tool. Neither do the four bulk transfer endpoints under `/api/transfer/`: they take only a transfer token from `open_notes_as_files` — not even a session cookie — and answer a token with `401` pointing at that tool (see **Bulk transfer** in the [API contract summary](./api.md)). Every other endpoint that requires a login is session-cookie only and answers a plain `401 unauthorized` to a Bearer request (endpoints that need no login at all, such as public share pages, simply ignore the header). In particular, tokens can **not** manage tokens (`/api/auth/tokens`) — not even change their own scope — and can **not** obtain a collaboration token for the live editor.
### Errors and rate limits
- `401 unauthorized` with a `WWW-Authenticate: Bearer …` header — no credentials, an unknown or expired token, or a token whose account is disabled. The header's `error` parameter is `invalid_token` when a Bearer token was sent but rejected, and absent when no credentials were sent — or when a non-Bearer scheme such as `Basic` was used (RFC 6750 §3).
- `403 insufficient_scope` — the token is valid but doesn't have the scope this endpoint needs (e.g. a read-only token calling `POST /api/notes`). This does **not** count against any rate limit.
- `429 too_many_requests` — token requests are rate-limited **per user, separately from browser sessions**: 300 reads per minute and 60 writes per 10 minutes by default (a site admin can lower these and the other per-user budgets named here, except the folder budget, which is fixed). The content endpoints add tighter budgets on top of those: `GET /api/notes/:id/content` and `GET /api/notes/:id/edits` share 120 reads per minute per user, and `POST /api/notes/:id/edits`, `POST /api/notes/:id/edits/:editId/revert` and `POST /api/notes` carrying `content` share 30 writes per minute per user. A token calling them burns from both budgets, so its real ceilings there are 120/min and 30/min, not 300/min and 60/10 min. The tighter budgets are keyed by user, not by credential, so they apply to browser sessions too, not just tokens — though in practice the web app never calls these endpoints itself; it reads and writes note content through the live collaboration connection instead. A runaway script cannot lock you out of the web UI. Invalid Bearer attempts are additionally limited per IP (30 per minute). A `429` to a request made with a token carries no `WWW-Authenticate` header and no `Retry-After`; the transfer endpoints' `429`s do carry `Retry-After` — every `429` of the four bulk transfer endpoints, and the `429` the two upload endpoints answer once invalid Bearer attempts from an IP are used up. The MCP tools draw on the same 120-reads-per-minute and 30-writes-per-minute budgets: `read_note_outline`, `read_note_section`, `read_note_image` and `create_transfer_token` with `download` on the 120-reads-per-minute one, `edit_note` (except `rename`), `create_note` carrying `content`, `copy_note`, `delete_note` and `restore_note` on the 30-writes-per-minute one. `create_folder`, `rename_folder`, `move_folder`, `move_note_to_folder` and `delete_folder` draw instead on a separate folder budget, 600 per minute per user, which the web app's folder changes share. `create_transfer_token` with `upload` draws on the 60-writes-per-10-minutes token budget. `copy_note` also draws on the upload budget, 120 files per 10 minutes per user, one per image it copies. `list_notes`, `search_notes`, `list_trash`, `list_folders` and `open_notes_as_files` draw on neither; `search_notes` has its own budget, 60 searches per minute per user; `GET /api/search` draws on the same budget and answers with HTTP `429` when it runs out. The commands `open_notes_as_files` returns draw on budgets of their own: a download on 10 per user per hour, fetching one note on the 120-reads-per-minute content-read budget, and a file sent back on 30 per user per minute — or, when the file changes nothing, on the 300-reads-per-minute token budget. Because one `POST /api/mcp` can carry several tool calls, these are counted **per tool call**, not per request (#108). Of the per-user MCP budgets, only the 300-reads-per-minute one answers `429` at the HTTP layer — `POST /api/mcp` always declares `notes:read`, so every token request to it draws from that bucket regardless of which tools it calls. The others — the 60-writes-per-10-minutes budget, the tighter per-tool-call budgets above, the folder budget, the 60-searches-per-minute one (for `search_notes`) and the upload budget — all answer as a tool error carrying the same `too_many_requests` code in a `200` JSON-RPC result instead.
- `503 server_busy` — on `POST /api/notes/:id/edits`, `POST /api/notes/:id/edits/:editId/revert`, and MCP's `edit_note` (except `rename`): another write to the same note held the per-note queue for longer than 10 seconds. Nothing was applied and nothing was recorded; retry. On MCP this is a tool error, not an HTTP `503`. `create_note`'s queue timeout is not in this list — it answers `internal` instead, same as `POST /api/notes` (see [AI editing](./ai-editing.md#limits)). MCP's `create_note`, `move_note_to_group`, `copy_note`, `delete_note`, `restore_note` and its five folder tools that change something — `create_folder`, `rename_folder`, `move_folder`, `move_note_to_folder` and `delete_folder` — can also answer `server_busy`, for a different reason: the server was busy (a lock or serialization conflict); nothing was created, moved, copied, restored or otherwise changed.
## Revoking
**Settings → Authorization → API tokens**, then the red trash-can button at the end of the token's row. Revocation deletes the token and takes effect immediately — the next request with it gets `401`. Transfer tokens it issued through `create_transfer_token` and `open_notes_as_files` stop working at the same moment. There is no undo; create a new token instead.
Expired tokens stay in the list (marked *Expired*) so you can see what a program was using; they are removed automatically once they have been expired for more than 30 days, and OAuth apps that have not been used for 30 days are removed along with their credential. There is no scheduler: the clean-up runs opportunistically, on any of six triggers — creating a personal token, registering an app, opening the consent page (an authorization request), deciding on it (pressing Allow or Deny), exchanging a code for a token, and refreshing a token.
Revoking an OAuth app's credential here also, eventually, removes its registration (see [Troubleshooting](#troubleshooting) below) — once that has happened, an app you want back has to be added again, not just re-authorized.
## Changing a token's access
In **Settings → Authorization → API tokens**, the pencil button on a token's row turns on or off **Edit**, **Move** and **Delete** for that credential; the change applies from its next request. A token or app that existed before `notes:delete` did doesn't get it by itself: turn it on there, or authorize the app again and tick it. An app can also be authorized again: the consent page starts from the access it has now, and what you tick there replaces it (see [Authorizing an app instead](#authorizing-an-app-instead-oauth)).
A credential's scope can also be changed in place, from a signed-in browser session, with `PATCH /api/auth/tokens/:id` and a body such as `{"scope": "notes:read notes:write"}` — the scope is one of `notes:read`, `notes:read notes:write`, `notes:read notes:write notes:move`, `notes:read notes:write notes:delete` and `notes:read notes:write notes:move notes:delete`. Like everything under `/api/auth/tokens`, it doesn't accept a token (see the [API contract summary](./api.md)). It works on personal tokens and app credentials alike, raises or lowers access regardless of what an app originally asked for, and applies from the credential's next request; an app's later token refreshes keep the new scope. An MCP client keeps the tool list it already has until it is restarted (see [Known limitations](./known-limitations.md)). Lowering a credential below `notes:write` also stops the upload tokens it issued through `create_transfer_token`, and the bulk transfer tokens it issued through `open_notes_as_files` from sending files back: using one for that answers `403 insufficient_scope` for as long as the credential can't write.
## Authorizing an app instead (OAuth)
An MCP client that supports OAuth does not need a pasted token. When it first calls `/api/mcp` it gets a `401` that tells it where the authorization server is; it registers itself (dynamic client registration — no secret, and its `redirect_uri` must be a loopback address on your own machine), opens your browser at the consent page, and once you press **Allow** it exchanges the one-time code for its own credential. That credential shows up in **Settings → Authorization → API tokens** as an *App* row, next to your personal tokens, and is revoked the same way. Re-authorizing the same app replaces its previous credential rather than adding another.
**Choosing what the app gets.** Reading is always included. For each further scope the app asks for, the consent page shows a box — *Create and edit notes* (`notes:write`), *Move or copy notes into groups* (`notes:move`) and *Move notes to the trash and restore them* (`notes:delete`; the last two can only be ticked together with the first) — and the app gets only what you tick. The first time you authorize an app — which includes after you revoked its credential, and after it registered again — the boxes start unticked, so pressing **Allow** straight away gives it read-only access. When the same app asks again, the boxes start from the access it has now, limited to what it is asking for this time, and are marked *Currently granted*; if it has access now that it isn't asking for this time, the page says so, and allowing leaves it only what you tick. Unticking a box there lowers the app's access. The `scope` in the app's token response is what was actually granted. The `/api/mcp` challenge lists all four scopes, so an MCP client that follows it asks for all four.
**What the consent page tells you, and why it matters:** the app's name is whatever the app said it was — it is *not* verified. What you can trust is the redirect address shown on the page: it is always a loopback address (`127.0.0.1`, `localhost` or `[::1]`), so only a program running on the computer where the browser is can receive the code. Only press Allow when you yourself just started that program. Denying (or hitting the credential limit) discards the request; to try again, start over from the app.
**When an app expires.** The **Expires** date on an *App* row is 30 days after the app was last used — or after you authorized it, if it hasn't been used yet — and moves forward by itself while the app is in use. Once it has passed, the app is removed at the next clean-up (see [Revoking](#revoking)); until then, if the app reconnects, the date moves forward again. An app that another account is still using stays until you revoke it. The access token itself lasts 24 hours and the app renews it on its own with its refresh token, so you don't need to do anything about that.
The exact command for each client, and what you should see once you press Allow, are in [MCP](./mcp.md#how-to-connect).
## Troubleshooting
- **"This application's registration with Knotebook has expired or does not exist."** — a plain-text page instead of the consent screen. The app is presenting a registration this server no longer recognizes: a registration is dropped once it is more than 24 hours old and has no live credential and no authorization code on record (even an expired one buys it one more cleanup pass) — which includes a registration whose credential you revoked in Settings, and one whose authorization was never exchanged for a credential — and apps unused for 30 days are dropped along with their credential. **The client does not recover on its own** — `mcp-remote`, for example, gets `401`, fails its refresh, reopens the authorize URL with the same stale `client_id`, lands back on this page, and then just sits on "Waiting for authorization…" until it times out. Recover by clearing the client's cached registration and starting over: for Claude Code's direct transport, run `claude mcp remove -s user knotebook` and add it again — it registers fresh. For `mcp-remote`, delete that server's cached files under `~/.mcp-auth/mcp-remote-v1/` (or the whole directory) and start the client again.
- **Another window of the same app suddenly asks you to authorize again.** Re-authorizing an app replaces its previous credential, so a second instance that shared the old one (e.g. a second Claude Code window) gets `401` and its refresh fails. Let it run the authorization flow once more. The consent page starts from the access the app has now; unticking a box there lowers it.
- **The consent page says the request has already been used or has expired.** Requests live for 10 minutes and are single-use; signing in (especially via SSO) can eat into that. Start again from the app.
- **Pressing Allow gives "Token limit reached".** You already hold the maximum number of credentials (20 by default; a site admin can lower it). Revoke one in Settings → Authorization, then start again from the app — the request you were on has been consumed.
- **Claude Code says `Couldn't complete authentication for "knotebook": Refusing to send credentials to non-https token endpoint '…'. OAuth token requests MUST use TLS …`.** Its built-in OAuth client gets all the way through consent and the browser callback, then refuses at the last step to send the token exchange to a plain `http://` authorization server (loopback is exempt; your deployment isn't) — `/oauth/token` never sees the request. Either serve the deployment over `https://`, or, on a plain-http LAN deployment, connect with a personal API token instead (see [How to connect](./mcp.md#how-to-connect)).
- **`mcp-remote` says `Non-HTTPS URLs are only allowed for localhost or when --allow-http flag is provided`.** Same requirement, checked up front against the server URL you gave it. `--allow-http` only lets the connection through; the OAuth token exchange is still refused, so on a plain-http deployment connect with a personal API token instead (the `mcp-remote` setup for a token still needs `--allow-http`; see [How to connect](./mcp.md#how-to-connect)) (#159).
## Security notes
- **Changing your password does not revoke your tokens.** They are independent credentials, the same way personal access tokens work on GitHub and similar services. If you think a token has leaked, revoke it here — changing your password will not stop it. An admin resetting your password with a reset link doesn't revoke them either.
- **A token that never expires never stops working until you revoke it.** If you don't need a permanent token, pick an expiry.
- **Treat a token like a password.** A `notes:write` token can **rewrite the content of any note you can edit** — not just create new ones — and those writes go straight into the live document everyone is looking at. A leaked one is a credential that acts as you, on your notes. A successful API write that changes a note's content is recorded, listed by `GET /api/notes/:id/edits`, and can be reverted while the note has not moved on — but a note keeps at most 100 entries. A rename, a move and a copy are not recorded; a move removes the note's per-person shares, public link and version history, which don't come back, and a single note can't be moved back out of a group. Prefer `notes:read` for anything that only needs to read, leave `notes:move` off unless a program really has to move notes into groups or copy them there — it is the scope that can hand a note over to a group for good — leave `notes:delete` off unless it has to move notes to the trash or delete folders, and don't paste a token into shared config, logs, or chat.
- **An admin disabling your account also stops your tokens** — every token request re-checks the account's status.
- The server never logs a token's plaintext or the `Authorization` header.
## Agent display name
Each credential carries a short **agent name** — the name a program is shown under inside the app. It appears on the AI's cursor label while it is working on a note, in the note header's row of people, on that note's last-edited line, and on every row of its **AI edit history**.
By default it is derived from the credential's own name: the first word, lowercased, with anything outside letters, digits, `.`, `_` and `-` removed — so `Claude Code (knotebook)` becomes `claude`, and an app registered as `MCP CLI Proxy` becomes `mcp`. If nothing usable is left (a name written entirely in Chinese, say) it becomes `agent`.
The pencil button at the end of a token's row in **Settings → Authorization → API tokens** edits it (the **Label** column), for personal tokens and authorized apps alike. Letters, digits, `.`, `_` and `-`, up to 32 characters; clear the field to go back to the derived name. Everyone who can see the note sees this name, so it is worth making it recognizable rather than clever.
Re-authorizing an app keeps the name you gave it, as long as the app comes back with the same registration — an app that had to register again (see [Troubleshooting](#troubleshooting)) arrives as a fresh credential and starts from the derived name. Renaming is also available over the API (`PATCH /api/auth/tokens/:id`, session-cookie only, like everything else under `/api/auth/tokens`); the full rules are in [AI editing](./ai-editing.md#agent-display-name).
See also: [MCP](./mcp.md) · [AI editing](./ai-editing.md) · [API contract summary](./api.md) · [Known limitations](./known-limitations.md).