Drive Mindmap.io maps via the node API
This skill teaches you to drive Mindmap.io maps by calling
its HTTP node API directly. It is the alternative to the mindmapio-mcp server:
same primitives, no MCP client required, just authenticated HTTP calls.
The API is the authoritative contract — match the shapes in the examples
exactly. A map's data is a tree of nodes, the same shape every read returns,
with camelCase fields like nodeType and parentId. The single-node write
endpoints take their own snake_case fields like node_type.
Setup
Every call sends a personal access token as a bearer header. Generate the token once in Mindmap.io: settings → API access. It acts as its user; regenerating it immediately revokes the old one.
export MINDMAP_API_TOKEN="<your personal access token>"
export MINDMAP_API_BASE_URL="https://mindmap.io" # optional, this is the default
Never print, echo, or commit the token. Send it on every request:
Authorization: Bearer $MINDMAP_API_TOKEN
A reusable curl alias for the examples below:
mm() { curl -sS -H "Authorization: Bearer $MINDMAP_API_TOKEN" \
-H "Content-Type: application/json" "$MINDMAP_API_BASE_URL$@"; }
Primitives
IDs are unprefixed everywhere. You mint node ids client-side, so you can reference a node id you chose before it is persisted.
Node content is sent as messages: a UIMessage array (Vercel AI SDK shape).
Each message is {role, parts} and each part is {type: "text", text}. A
node's body — a prompt, a data node, a note's source turn — is a single user
message with one or more text parts.
Read
List maps — GET /api/mindmaps → array of {id, title, kind, created_at, updated_at}, newest first.
mm /api/mindmaps
Read a full map — GET /api/mindmaps/{id} → {id, title, kind, user_id, data} where data is {rootId, selectedId, nodes} and nodes maps id → node.
mm /api/mindmaps/MAP_ID
Read one node — GET /api/mindmaps/{mapId}/nodes/{nodeId} → a SubtreeNode {node, children} with children: []. The node's own children array still lists child ids for navigation.
mm /api/mindmaps/MAP_ID/nodes/NODE_ID
Read a subtree — GET /api/mindmaps/{mapId}/nodes/{nodeId}/subtree?depth=N → nested SubtreeNode. Omit depth for the full subtree; 0 returns just the node, 1 the node and its immediate children.
mm "/api/mindmaps/MAP_ID/nodes/NODE_ID/subtree?depth=2"
Structural write
Create a map — POST /api/mindmaps with {title?, kind?, data?}. Pass an initial data tree with a root node so later node primitives have a parent to hang from. → 201 {id, title?, kind?}.
Each node has a type. A data node holds content you keep — a topic, reference text, a note. A prompt node is a question you run against a model with submit. The root usually holds the map's subject, so make it a data node:
mm /api/mindmaps -X POST -d '{
"title": "My research map",
"data": {
"rootId": "root",
"nodes": {
"root": {
"id": "root",
"nodeType": "data",
"messages": [{ "role": "user", "parts": [{ "type": "text", "text": "Topic" }] }],
"children": []
}
}
}
}'
Create a node — POST /api/mindmaps/{mapId}/nodes with {nodeId, parentId, position?, data?}. nodeId must be unique within the map; mint a uuid yourself. data may carry {messages, note, node_type}, where messages is the node's content as a UIMessage array. The node is born structural (a draft); running it is a separate generative call. → 201 full node.
mm /api/mindmaps/MAP_ID/nodes -X POST -d '{
"nodeId": "q1",
"parentId": "root",
"data": {
"messages": [{ "role": "user", "parts": [{ "type": "text", "text": "What is X?" }] }],
"node_type": "prompt"
}
}'
A data or note node carries its body the same way — a user message in messages:
mm /api/mindmaps/MAP_ID/nodes -X POST -d '{
"nodeId": "d1",
"parentId": "root",
"data": {
"messages": [{ "role": "user", "parts": [{ "type": "text", "text": "Reference notes for the branch." }] }],
"node_type": "data"
}
}'
Update a node — PATCH /api/mindmaps/{mapId}/nodes/{nodeId} with any subset of {messages, note, node_type, is_collapsed, model_provider, model_id}. Send messages to replace the node's content. Pure tree mutation, no LLM call, no metering. → 200 {success}.
mm /api/mindmaps/MAP_ID/nodes/q1 -X PATCH -d '{
"messages": [{ "role": "user", "parts": [{ "type": "text", "text": "What is X, precisely?" }] }]
}'
Delete a node — DELETE /api/mindmaps/{mapId}/nodes/{nodeId}. Cascades to descendants and reindexes siblings. The root cannot be deleted. → 200 {success}.
mm /api/mindmaps/MAP_ID/nodes/q1 -X DELETE
Delete a map — DELETE /api/mindmaps/{id} → 200 {success}.
mm /api/mindmaps/MAP_ID -X DELETE
Generative
Submit a node — POST /api/mindmaps/{mapId}/nodes/{nodeId}/submit with optional {prompt?, modelId?}. Runs the LLM and blocks until no ancestor is still generating, then returns the completed node {nodeId, status, messages}. prompt supplies the user text when the node has none yet; omit it to run the stored text. modelId overrides the house model. Metered; over budget returns 429.
mm /api/mindmaps/MAP_ID/nodes/q1/submit -X POST -d '{}'
Auto-expand a node — POST /api/mindmaps/{mapId}/nodes/{nodeId}/auto-expand with optional {count?, direction?} (count 1–4, default 2). Generates follow-up prompts as queued child nodes and returns {nodeId, childIds}. One level only — it does NOT run the children. Metered; over budget 429.
mm /api/mindmaps/MAP_ID/nodes/q1/auto-expand -X POST -d '{"count": 3}'
Retry a node — POST /api/mindmaps/{mapId}/nodes/{nodeId}/retry?force=true with optional {prompt?, modelId?}. Clears the prior error and re-runs, returning {status, messages}. Retrying an expand node that already has children returns 409 unless force=true (which deletes those children first). Metered; over budget 429.
mm "/api/mindmaps/MAP_ID/nodes/q1/retry?force=true" -X POST -d '{}'
Interrupt a node — POST /api/mindmaps/{mapId}/nodes/{nodeId}/interrupt. A generating or queued node flips to interrupted with partial output preserved. Idempotent; not metered. → 200 final node.
mm /api/mindmaps/MAP_ID/nodes/q1/interrupt -X POST
Publish
Publishing flips a map from private to public and mints a non-guessable public id (slug). Owner-only; not metered. This is how you turn a map you built into a shareable link or an iframe embed.
Publish a map — POST /api/mindmaps/{mapId}/publish. Mints a public id if
the map has none (re-publishing reuses the existing one) and flips visibility to
public. → {publicId}.
mm /api/mindmaps/MAP_ID/publish -X POST
# → {"publicId":"G_N4wWD2TiUIoHuO"}
Unpublish a map — DELETE /api/mindmaps/{mapId}/publish. Flips back to
private, which immediately 404s every public/embed link (the revoke
mechanism). The public id is retained for re-publishing. → {success:true}.
Build the shareable links from the publicId. The query frames the map on first
paint: node=root, the semantic zoom level (full default; keyword/phrase
open a large reference map zoomed-out so its whole shape shows at a glance),
and cz, the canvas-zoom percent.
Viewer link: https://mindmap.io/app/<publicId>?node=root&zoom=<level>&cz=<percent>
Iframe embed: https://mindmap.io/app/embed/<publicId>?node=root&zoom=<level>&cz=<percent>
So a zoomed-out reference map embeds as
https://mindmap.io/app/embed/G_N4wWD2TiUIoHuO?node=root&zoom=keyword&cz=90, and
a normal example/template map as …?node=root&zoom=full&cz=100.
Via the MCP server,
publish_mapdoes both steps in one call — it publishes and returns{publicId, viewerUrl, embedUrl}already framed by thezoom/czyou pass.unpublish_mapreverses it.
Pattern: agent-drives-recursion for auto-expand
Auto-expand does NOT recurse and does NOT run the children it creates — it only
queues one level of follow-up prompts. You drive the recursion: call
auto-expand, then submit each returned child yourself, and recurse if you
want to go deeper.
# 1. Fan a completed node out into queued follow-up children.
resp=$(mm /api/mindmaps/MAP_ID/nodes/q1/auto-expand -X POST -d '{"count": 3}')
# 2. Submit each returned child to run it (each blocks on its ancestors).
echo "$resp" | jq -r '.childIds[]' | while read -r child; do
mm "/api/mindmaps/MAP_ID/nodes/$child/submit" -X POST -d '{}'
done
# 3. To go deeper, auto-expand a child and repeat. Choose your own depth/breadth
# budget — there is no server-side recursion or fan-out cap beyond count 1-4.
Submit blocks until ancestors finish, so it is safe to submit children in sequence; the generation gate guarantees each child sees complete parent context.
Errors
Responses carry {error} on failure. Common statuses: 401 (no/revoked
token), 403 (not the token user's map), 404 (missing map/parent/node), 400
(malformed, or deleting the root), 409 (duplicate node id, or re-expanding
without force), 429 (over budget — body carries an upgrade/buy-credits CTA).
On 429, stop generating and surface the CTA rather than retrying blindly.
Hello world
# create a map whose root is a data node holding the topic
MAP=$(mm /api/mindmaps -X POST -d '{"title":"Hello","data":{"rootId":"root","nodes":{"root":{"id":"root","nodeType":"data","messages":[{"role":"user","parts":[{"type":"text","text":"Hello"}]}],"children":[]}}}}' | jq -r '.id')
# add a prompt node under the root, then run it
mm /api/mindmaps/$MAP/nodes -X POST -d '{"nodeId":"q1","parentId":"root","data":{"messages":[{"role":"user","parts":[{"type":"text","text":"Say hi in one word."}]}],"node_type":"prompt"}}'
# run it and read the answer back
mm /api/mindmaps/$MAP/nodes/q1/submit -X POST -d '{}'
For the MCP-server route instead, see the project README.