---
title: "Call the updateDocument MCP tool"
method: POST
path: "/api/v1/actions/updateDocument"
tags: ["Actions"]
---

# Call the updateDocument MCP tool

`POST /api/v1/actions/updateDocument`

## Request

- Base URL: `https://app.reachium.io`
- URL: `https://app.reachium.io/api/v1/actions/updateDocument`
- Auth: HTTP bearer

Edit an existing document's title and/or content, replacing whichever field you pass, and/or publish it. If the document is public (is_public: true), an edit changes its LIVE page immediately: anyone with the /p/ link sees the new content right away. Separately, and regardless of is_public, any lead-magnet link createLeadMagnetCampaign already delivered to a lead (a private per-lead link) also shows the new content the next time that lead opens it, since that page reads the document's current content at open time rather than a frozen copy. Caution: if the document is open in the Reachium editor right now, the user must reload that tab before typing or their autosave overwrites this edit; the response warnings say when it was saved recently. If anyone is CURRENTLY in a live Collaborate room on this document, a title/content_markdown/edits write is refused outright (reason open_in_collaboration), naming who is in the room, rather than risking a silent overwrite by the room's next autosave; ask them to close it or wait for the room to go idle, then retry. If the collaboration check itself cannot be confirmed (a Liveblocks outage), the write proceeds and the response carries a collab_check_failed warning instead. Refuses documents that are restricted to admin editing (a Reachium-internal template a workspace cannot edit here) and documents outside this workspace. Provide at least one of title, content_markdown (up to 60000 characters), edits, or publish:true. Two ways to change the body: content_markdown REPLACES it entirely (embeds/tables the markdown cannot express are lost); edits applies PARTIAL changes to the current body (replace/insert/delete a section by heading, replace exact text in one line, insert/replace/remove images), leaving everything else, including tables and embeds, untouched. Prefer edits for revisions: call getDocuments with document_id first, then target sections by heading or section_index. publish:true makes the document a public /p/ page: pass it alone to publish an already-written document with no other change, or pass it together with title/content_markdown/edits to apply that edit first and publish the result in the same call, so the live page never shows stale content. unpublish:true takes a published /p/ page offline again: the document is kept as a private draft and its slug is retained, so a later publish:true restores the exact same URL. It is standalone (no title/content/edits in the same call) and mutually exclusive with publish:true. archive:true retires a document without deleting it (the escape valve for docs that campaigns already delivered, which can never be deleted): it moves to the Archived folder and drops out of default getDocuments listings, single-call, no confirm needed, fully reversed by archive:false. Archiving does NOT unpublish - a still-published doc keeps its live /p/ page and the response warns about it. Standalone like unpublish. publish:true and unpublish:true are both two-step, same shape as createDocument's: the FIRST call (no confirm_token) returns a preview naming the document and its public URL and creates/changes nothing; call again with the identical arguments plus confirm_token to actually execute. A plain edit (publish and unpublish unset or false) stays single-call, no confirm_token needed, byte-for-byte the same as before publish:true existed on this tool.

Called through the Actions bridge: the response is that tool's own result, wrapped as `{ data: <tool result> }` under HTTP 200. Billing, daily-cap, and confirm-step outcomes are embedded INSIDE `data`, never remapped to an HTTP status (see the templated `/api/v1/actions/{toolName}` operation above for the full protocol, and `x-mcp-actions` for this tool's machine-readable scope/cap/confirm_behavior fields). Authentication, request-shape, and optional idempotency-protocol failures still use their documented non-200 statuses.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `document_id` string, uuid, required: The document to update (from getDocuments or a createDocument result).
  - `title` string: New title (titleFirstLine truncates to 120 chars regardless, so this is the true bound). Omit to leave unchanged.
  - `content_markdown` string: New full document body in markdown; REPLACES the existing content entirely (not a patch). Omit to leave unchanged. Supported: headings (# through ######), paragraphs, bullet and numbered lists (a blank line between numbered items keeps counting), pipe tables (| a | b | header, | --- | --- | separator, then rows; \| for a literal pipe in a cell), **bold**, *italic*, [text](url) links, `inline code`, fenced ``` code blocks, > blockquotes, --- horizontal rules, - [ ] / - [x] checklists, and images on their own line as ![caption](https://...) (an https URL; inline images inside a sentence are not converted). Highlights and colors: ==text== is a yellow highlight, ==<color>:text== a colored highlight, @@<color>:text@@ colored text, with <color> one of gray, brown, red, orange, yellow, green, blue, purple, pink OR a custom #RRGGBB hex (exactly 6 hex digits, e.g. ==#D1FAE5:win== or @@#B91C1C:warning@@; anything else renders as literal text). Marks must hug their text (==key point==, never == key point ==), and may wrap bold or italic (==**key**== is bold AND highlighted). Layout: end a paragraph, heading, or list line with a space plus {center}, {right}, or {justify} to align it; images take ![caption](url){width=420} (pixels, 50-1200) and/or an alignment token like {width=420 left} (images default to centered). Pull getPlaybook("content", "document-styling") BEFORE styling a document: the discipline (what to highlight, which colors mean what, the readable palette, when to center or size) lives there. NOT supported (renders broken or missing, use plain formatting instead): nested or indented list levels (they all flatten to one level), setext headings (a title on its own line followed by a row of ==== or ----), bold nested inside italic or italic nested inside bold (the ** or * marks stay visible instead of rendering), and per-character font sizes (use heading levels for size hierarchy).
  - `edits` object[]: PARTIAL edits applied in order, all-or-nothing, to the CURRENT document (read it first with getDocuments document_id). Mutually exclusive with content_markdown. On failure the response names failed_edit_index and nothing is written.
    - `op` 'replace_section' | 'insert_section' | 'delete_section' | 'replace_text' | 'insert_image' | 'replace_image' | 'remove_image' | 'move_section', required: replace_section: swap a heading + its body for content_markdown (keeps the old heading if your markdown has none). insert_section: add content_markdown at `where`. delete_section: remove a heading + its body. replace_text: exact `find` -> `replace` inside ONE block/line, must match exactly once. insert_image / replace_image / remove_image: manage image blocks. move_section: cut a heading + its body out of this document and insert it into target_document_id (at where / target_section); must be the only edit in the call.
    - `section` string: Heading text of the target section (case-insensitive). A section = that heading plus everything until the next heading of the same or higher level.
    - `section_index` integer: 1-based heading index from getDocuments(document_id).sections; wins over `section`. Use it when two headings share the same text.
    - `where` 'before' | 'after' | 'start' | 'end': Placement for insert_section / insert_image. With a section: before = before its heading, start = right under its heading, after/end = after its last block. Without a section: start = top of the document, end = bottom (default end).
    - `content_markdown` string: New markdown for replace_section / insert_section. Same subset as the full-body field, images allowed as ![caption](https://...) lines.
    - `find` string: replace_text: exact text as it appears in one line of the getDocuments markdown (marks like ** included).
    - `replace` string: replace_text: replacement markdown (empty string deletes the found text).
    - `image_url` string, uri: insert_image / replace_image: public https URL of the image; fetched server-side (5 MB max) and re-hosted in Reachium storage.
    - `image_base64` string: insert_image / replace_image: raw base64 image bytes (png/jpeg/webp/gif, 5 MB decoded max). Pass exactly one of image_url, image_base64, or asset_name.
    - `asset_name` string: insert_image / replace_image: name of a previously uploaded asset from listAssets. Loaded straight from storage - no bytes through the model, no re-fetch over http. Pass exactly one of image_url, image_base64, or asset_name.
    - `caption` string: insert_image / replace_image: caption under the image (replace_image keeps the old caption when omitted).
    - `current_image_url` string, uri: replace_image / remove_image: the exact url from getDocuments(document_id).images.
    - `target_document_id` string, uuid: move_section: the document that receives the section.
    - `target_section` string: move_section: heading in the TARGET document that `where` is relative to; omit to use start/end of the target.
  - `publish` boolean: true to make the document a public /p/ page, which is two-step: the first call previews and returns a confirm_token, and nothing changes or publishes until you send it back. Pass alone to publish with no other change, or together with title/content_markdown/edits to edit then publish in one flow. Omit or pass false for a plain, single-call edit.
  - `unpublish` boolean: true to take a published /p/ page offline: the document is kept as a private draft and its slug is retained, so publishing again later restores the same URL. Two-step like publish (preview, then confirm_token). Standalone: pass it with no title/content_markdown/edits, and never together with publish. Already-sent lead-magnet delivery links are unaffected.
  - `archive` boolean: true to archive (retire) the document: it moves to the Archived folder and drops out of default getDocuments listings; false to unarchive. Single-call, no confirm_token, reversible. Does NOT unpublish a published doc (pair with unpublish:true for that). Standalone: no other fields in the same call.
  - `slug` string: Custom URL slug for the public /p/ page; only valid together with publish:true. Lowercased and hyphenated automatically, capped at 50 characters on a word boundary. For SEO: 3-6 words, the primary keyword FIRST, no filler words or dates (good: "linkedin-outreach-playbook"; bad: "my-2026-guide-v2-final"). Refused if another document already owns it (never silently suffixed). Omit to auto-generate from the title. On an ALREADY-public document a new slug moves the page: the old URL stops resolving immediately.
  - `folder` string: Move the document into a folder, by folder id or (case-insensitive) name (getDocuments lists them under `folders`); pass 'root' to move it back to the root. Single-call, combinable with title/content_markdown/edits, refused on an archived document (unarchive first) and not allowed in the same call as archive.
  - `confirm_token` string: From the preview response of a publish:true or unpublish:true call; resend with the same arguments to execute. Never needed for a plain edit.

## Response `200`

The result of calling this tool.

- object
  - `data` unknown, required
  - `error` unknown, required

## Other responses

- `400`: The request body was not a JSON object (code invalid_request), or Idempotency-Key is malformed (code invalid_idempotency_key).
- `401`: Missing or invalid API key. Send `Authorization: Bearer rmcp_...` (code unauthorized).
- `403`: This tool is out of the key’s granted scope (code insufficient_scope).
- `409`: This Idempotency-Key has an unresolved in-flight or ambiguous result (code request_in_flight). Reconcile before using a new key.
- `422`: This Idempotency-Key was already used with a different request body (code idempotency_key_reuse).
- `429`: Rate limited: shared `mcp:ip:{ip}` (600/60s) or `mcp:key:{keyId}` (120/60s) bucket, the SAME buckets the MCP endpoint uses (code rate_limited).
- `500`: Unhandled server error (code internal_error).

---

[API](https://skmtc.dev/reachium/apis/reachium-api.md) · [All operations](https://skmtc.dev/reachium/apis/reachium-api/llms.txt) · [OpenAPI document](https://skmtc.dev/reachium/apis/reachium-api/revisions/30c16b30b499?raw)
