- name
- anytype-api
- description
- Call Anytype's local HTTP API v2 directly — search, read, create and edit objects, queries, collections and chats over REST. Use when writing scripts, SDK code or curl against the API. For interactive note/task work prefer the `anytype` CLI and its skill (cmd/anytype/SKILL.md); this guide is the raw-HTTP layer beneath it.
# Anytype API v2 — HTTP guide for agents
Local REST API at `http://127.0.0.1:31009` (the Anytype app must be
running). Every call sends `Authorization: Bearer <key>`; keys are created
in the app (Settings → API keys) — **the API mints none**. Bodies are
compact JSON, and every name this API owns is `snake_case` — params,
fields and op names alike. (Inside an object's `blocks` and `properties`
you are reading the AnyBlock format's content; property and type keys
there are served as slugs by default, as display names under
`?keys=name` — see the properties bullet below.) Every list takes
`?offset=&limit=` (default 25, max 1000) and returns
`{data, total, offset, limit, has_more}`.
**First call: `GET /v2/auth/whoami`.** A key may be scoped to particular
spaces and to read-only. `grant.scoped: false` means the whole account;
otherwise `grant.spaces` lists what you may touch and `grant.permission`
whether you may write. Ask this instead of discovering limits through 403s
(`space_not_granted` / `write_not_granted` — the message names the grant).
## The data model in six ideas
- **Spaces** contain everything; nearly every route is
`/v2/spaces/{space_id}/…`. `GET /v2/spaces` lists them.
- An **object** = `properties` (typed key-values) + `blocks` (document
content). `type` is served as a type **key** (`page`, `task`) — never an
id; on input a type NAME (`"Meeting note"`) resolves too.
- **Properties** are addressed by key — a snake_case slug minted once at
creation and frozen, so it survives renames (`due_date`, `icon_emoji`,
`manual_property`). `GET …/properties` spells them that way, and served
documents spell them that way **by default**; `?keys=name` on a read
serves the display-name vocabulary instead (`"Due date"`) — pick slugs
for anything long-lived, names for showing a user their own words.
Input is forgiving everywhere and accepts BOTH: `dueDate`, `DueDate`,
`due-date`, the display name `Due date` and even `Дата выполнения` all
resolve to the one property they name; an input matching two properties
is a 400 listing both, and an unknown one never silently creates
anything. (One exception: the compact `filter` STRING validates before
folding — spell a multi-word name with underscores there, `Due_date`.)
Select/multi_select values are option **names** (`"In progress"`,
case-sensitive) — never option ids. A name the property does not already
hold is **refused** — check it against `GET …/properties/{key}/options`,
or resend with **`?create_missing_options=true`** to create it (a PATCH caps that
at 64). Unknown property keys are rejected with a did-you-mean.
- **Blocks** are a FLAT array in pre-order with an integer `indent`
(absent = 0) — no `children` key. Inline formatting is markdown inside
`text`. Use block ids exactly as a read served them.
- Title and description are **not blocks** — they live in `properties`
(`name`, `description`). A fresh object has zero blocks.
- A **query** is a live query over a type (its type key is still `set`, the
name it carries internally); a **collection** is a hand-curated
list (edited via `add_items`/`remove_items`). **Chats** store messages
outside blocks, paged by order-id cursors.
## Which operation
| Intent | Call |
|---|---|
| find objects | `POST …/{space_id}/search` (or `POST /v2/search` across spaces — rows then carry `space_id`). Search with filters; don't enumerate `GET …/objects` |
| watch a search live (new objects, unread mentions) | `POST …/{space_id}/search/stream` — SSE, same body without `query`; see Query |
| read one object | `GET …/objects/{id}` — start with `?outline=true` |
| change property values | PATCH op `set_properties` — `add`/`remove` for list values, `set` for scalars |
| complete a task object | `set_properties` (`"set":{"done":true}` or the status option) — a property, not a block edit |
| change a word/phrase | op `replace_text` `{find, replace}` — `id` optional; never retype the block |
| toggle a checkbox block | op `update_block` `{"match":"Draft timeline","set":{"checked":true}}` — merge; text untouched. `match` or `id`, never both |
| add content | op `insert_blocks` with a `markdown` payload — write markdown, the server parses it |
| restructure | ops `move_block` / `replace_subtree` / `delete_block` (`delete_block` takes `match` too) |
| one table cell | op `set_cell` — never rewrite the table |
| show/hide a view column, edit a view | op `update_view` — works on queries, collections and a type's default view (PATCH the type OBJECT id from `GET …/types/{key}`) |
| add / reorder / remove a view | ops `insert_view` (`copy_from` duplicates one) · `move_view` (`position:"first"` = default tab) · `delete_view` |
| create an object | `POST …/objects` — shortcut `{type, name, properties, markdown}` covers most cases |
| delete an object you created | `DELETE …/objects/{id}` — archives (Bin, reversible in the app). Only works on objects THIS key created after provenance shipped; anything else → 403 `not_created_by_this_key`, permanently — don't retry, archive in the app instead. Ownership is matched on the app name EXACTLY (byte-for-byte — re-pair under the identical name to keep delete rights). User content only: system objects 403. Probe first with `?dry_run=true` |
| curate a collection | PATCH ops `add_items` / `remove_items` on the collection object |
| read a query / collection | `GET …/queries/{id}/objects` · `…/collections/{id}/objects` (`?view=`, `?fields=`) |
| new type / property | `POST …/types` · `POST …/properties`; select options ride the property, or `?create_missing_options=true` mints them from values |
| upload a file | `POST …/files` (multipart or `{"url":…}`) → the id file blocks and chat attachments need |
| download a file or icon | `GET …/files/{file_id}/content`; use a file id or a space/member's `icon_image`. Optional `?width=` selects an image size. Supports ranges, conditional reads, and `HEAD`. |
| chat | `GET/POST …/chats/{id}/messages`, `POST …/read` — see Chats |
| put an object in the sidebar | `POST …/widgets` `{target, scope}` — `scope:"personal"` is this account's own sidebar (any writer); `scope:"space"` is the shared one (owner/admin; not offered in one-to-one spaces). `GET …/widgets` lists both; `PATCH`/`DELETE …/widgets/{id-or-target}` change or remove one. Not `is_favorite` |
| comment on an object | `POST …/objects/{id}/discussion` → `{id}`, then the chat routes with that id — see Chats |
## Read cheaply
- `GET …/objects/{id}?outline=true` → every block's `{indent, id, type}`
plus its text truncated to 80 runes — structure + addressable ids at a
fraction of the tokens. Follow up with `?block={id}` for one subtree, or
PATCH directly: **editing needs no prior full read once you know the
ids** (but copy exact text from a full read — outline text may be cut).
- When the request already quotes the text to change, skip the read
entirely: `replace_text {find, replace}` locates the block itself, and
`update_block`/`delete_block` take `match` for the same job (one match, or
a refusal listing the candidates).
- `?include=properties` or `?include=blocks` reads half the object.
`?format=md` is a read-only markdown rendering.
- Echo block ids back exactly as a read served them; if one is rejected as
unknown, re-read and use the fresh ids. `?ids=full` is the backup/export
shape — the read to archive or clone from, not needed for editing.
- List/search rows are minimal `{id, name, type}`; add columns with
`fields=` (property keys) instead of GETting each object. Search rows also
carry `discussion` (the object's comment-thread chat id) when it has one.
- Every object read returns an `etag` (envelope + `ETag` header).
## Edit: PATCH ops
`PATCH …/objects/{id}` body `{"ops":[…]}` — one atomic batch (≤512 ops,
≤256 blocks per op): any invalid op rejects the whole PATCH with
`ops[i]`-addressed issues. Fourteen ops:
```json
{ "ops": [
{ "op": "set_properties", "set": {"status": ["Done"]}, "unset": ["oldKey"],
"add": {"tags": ["urgent"]}, "remove": {"assignee": ["bafy…"]} },
{ "op": "update_block", "match": "Draft timeline", "set": {"checked": true} },
{ "op": "replace_text", "find": "Q3 report", "replace": "Q4 report" },
{ "op": "insert_blocks", "after": "b3", "markdown": "## Notes\n- first\n- second" },
{ "op": "move_block", "id": "b9", "inside": "b2", "position": "last" },
{ "op": "delete_block", "id": "b4", "recursive": true },
{ "op": "set_cell", "table_id": "t1", "row": "r2", "col": "c1", "value": "done" },
{ "op": "update_view", "columns": {"status": {"hidden": false}} },
{ "op": "insert_view", "name": "Board", "copy_from": "viewAll1",
"set": {"type": "kanban", "groupBy": "status"} }
] }
```
- **`set_properties`**: a key appears in at most one of
`set`/`unset`/`add`/`remove`. `add`/`remove` are per-entry list edits
(select/multiSelect/objects/files) — appending one tag never rewrites
the array. `remove` never creates the option it names. `set: {"k": []}`
= present-but-empty; `unset` removes presence.
- **`update_block`** is THE block-field op (merge; explicit `null` clears a
field) — checkbox, color, language, retype, or full text rewrite.
- **`match` addresses the block by its TEXT** on `update_block` and
`delete_block` — the `id` alternative: give one or the other, **never
both** (and never neither). The text must appear in exactly ONE block or
the op refuses: zero → read the outline, several → the error lists
candidate ids to retry with. Repeats inside the one matched block are
fine — `match` names a block, not an occurrence. It reads the document as
the ops before it in the batch left it.
- **`replace_text`**: `id` is optional — omitted, `find` locates the block
and must appear in exactly ONE block (zero or several matching blocks
refuse; the ambiguity error lists candidate ids to retry with). Within
the matched block `find` must match exactly once ("found 2 matches —
provide more context"); `replace_all: true` is the escape, within that
one block only. Preferred over `update_block` for word-level edits.
`replace_subtree {id, blocks}` swaps a block plus descendants.
- **`insert_blocks`**: `blocks` (flat array) or `markdown` — mutually
exclusive, same targeting. Target with one of `after`/`before`/`inside`
(+`position: first|last` inside that container); omit all three and
`position` picks an end of the DOCUMENT — `last` (or absent) appends,
`first` inserts at the start, both on an empty object too. Payload
`indent: 0` = the anchor's level (`after`/`before`) or the container's
child level (`inside`). `move_block` targets the same way, so
`{"op":"move_block","id":"b9","position":"first"}` moves a block to the top
of the document.
- **Author new content without ids** — an `id` names an EXISTING block, so
`insert_blocks` takes none anywhere in its payload (rows and columns
included); the server mints them and returns them in `created_blocks`,
keyed by the payload path that produced each — `ops[0].blocks[0]`,
`ops[0].blocks[0].rows[1]`, `ops[0].blocks[0].columns[0]`. The same holds
wherever you leave an id out of an existing-content payload (a new row in
`update_block set.rows`, a block inside a `set_cell` array), so you never
have to re-read to learn an id you just created.
- **`update_view`** edits ONE dataview view — never resend the views array.
`block`/`view` are optional when the object has one dataview and it one
view (types, queries, collections usually do). `set` merges view fields
(`name`, `type`, `groupBy`, `sorts`, `filters` — arrays replace whole;
`filter` takes the compact string; null clears a field); `columns` merges
per property key: `{"hidden": false}` shows a column, `null` removes it,
a new key appends one. Works on Blocks-restricted objects — view config
is not a block edit.
- **`insert_view`/`move_view`/`delete_view`** complete the family (same
addressing, same channels; insert_view's name is its own required field —
not in `set`). insert_view needs only `name` — bare default: every listed
property visible, newest first; `copy_from` duplicates a view (then
`set`/`columns` override); the minted id returns in `created_views`,
keyed `ops[i]`. move_view REQUIRES one of `after`/`before`/`position`
(`"first"` = default tab). delete_view refuses the last view — insert the
replacement first (one atomic batch swaps a bad default view).
- Response: new `etag`, `created_blocks` (payload position → real id;
nested row/column/cell slots included), `created_views` (same, for minted
view ids), `created` (options minted under `?create_missing_options=true`),
`diff_stats {blocks_added, blocks_removed, blocks_changed, blocks_moved,
properties_changed}`, `warnings` (advisory, e.g. an unguarded date filter).
- **There is no whole-document replace** — never read a document,
regenerate it and write it back. Replace a section with
`replace_subtree`; start over by batching `delete_block`s with the new
`insert_blocks`.
## Query
`POST …/search` body: `{query?, type?, filter?|filters?, sorts?, fields?}`.
Pagination is the query params — a body `limit` is rejected. Search is a
read: no `Idempotency-Key`, `dry_run` ignored.
```json
{ "query": "report", "type": "task",
"filter": "done = false AND (due_date < currentWeek() OR due_date IS EMPTY)",
View on GitHub