> ## Documentation Index
> Fetch the complete documentation index at: https://developers.bottari.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Update content with Markdown

> Insert, replace, or edit a document's content as Markdown text. Requires Update content.

Edits a document's content as text. Requires **Update content**. Creating a subpage with a `<page>` tag also requires **Insert content**.

Set `type` to one operation and send that operation's object under the same key.

**Authorization** `string` *required*

`Bearer <token>`. See [Tokens](https://developers.bottari.ai/auth/tokens.md).

**Bottari-Version** `string`

Optional. Only `2026-03-11` is accepted. Without it, the current version is used. A different value returns 400.

**id** `string` *required*

A document ID. A block ID or database ID returns 400.

**type** `string` *required*

`insert_content`, `update_content`, `replace_content_range`, or `replace_content`.

**markdown\_version** `string` (default: v2)

Only `v2` is accepted. `v1` returns 400.

**allow\_async** `boolean`

`true` returns 202 with a task. See [Retrieve an async task](https://developers.bottari.ai/api-reference/async-tasks/retrieve.md).

## Operations

### insert\_content

Adds content to the document.

**insert\_content.content** `string` *required*

The Markdown to add.

**insert\_content.position** `object`

`{ "type": "start" }` or `{ "type": "end" }`. The default is the end.

**insert\_content.after** `string`

Adds it on a new line after this text, instead of using `position`. Uses the same pattern as `content_range`.

### update\_content

Replaces pieces of text. This is the safest way to edit a document people may be typing in, because lines you don't change are left alone.

**update\_content.content\_updates** `object[]` *required*

Applied in order. Each has `old_str`, `new_str`, and optional `replace_all_matches`.

**update\_content.allow\_deleting\_content** `boolean`

`true` lets the edit remove subpages and inline databases.

* `old_str` matching more than once returns 400 unless `replace_all_matches` is `true`.
* If no `old_str` matches, the response is 400. If only some don't match, the others are applied and the response has `warnings` with code `no_match`.

### replace\_content\_range

Replaces one stretch of text.

**replace\_content\_range.content\_range** `string` *required*

`start text...end text`, or one piece of text. There must be exactly one match.

**replace\_content\_range.content** `string` *required*

The new Markdown.

**replace\_content\_range.allow\_deleting\_content** `boolean`

`true` lets the edit remove subpages and inline databases.

### replace\_content

Replaces the whole content.

**replace\_content.new\_str** `string` *required*

The new Markdown.

**replace\_content.allow\_deleting\_content** `boolean`

`true` lets the edit remove subpages and inline databases.

> [!WARNING]
>
> `replace_content` has no version check. Anything a person typed after you read the document is overwritten. Use `update_content` when people may be editing.

## How matching works

`old_str`, `after`, and `content_range` match the text from [Retrieve as Markdown](https://developers.bottari.ai/api-reference/pages/markdown.md), not the text you sent earlier. Bold text reads as `<b>text</b>`, so `**text**` doesn't match it. Read the document first.

Uploaded file URLs change every time you read them. Match `![](` and not the full URL. `replace_content` compares files by the file itself, so it keeps them.

## How edits are saved

The new text is compared to the current content, block by block.

* Blocks with the same content are left alone and keep their IDs.
* A changed block keeps its ID, even if its type changes.
* A removed block goes to the trash and can be restored. See [Delete a block](https://developers.bottari.ai/api-reference/blocks/delete.md).
* Mentions always send notifications. `notifications` isn't accepted here.

## Markdown format

Send what [Retrieve as Markdown](https://developers.bottari.ai/api-reference/pages/markdown.md) returns. Reading a document and writing the same text back changes nothing.

| Content | Markdown |
| - | - |
| Headings | `#` to `####` |
| Bold, italic, strikethrough, underline, code | `**text**` or `<b>`, `*text*` or `<i>`, `~~text~~` or `<s>`, `<u>`, `` `text` `` |
| Link | `[text](https://example.com)` |
| Equation | `$E = mc^2$`, or a `$$` block |
| Text color | `<span color="green">text</span>` |
| Background color | `<p color="blue_bg">text</p>` |
| Empty line | `<p />` |
| Blocks inside a paragraph | `<p>` with the paragraph, then its children |
| Lists | `- item`, `1. item`, `- [ ] task`, `- [x] task` |
| Toggle | `<toggle>` with the first line as the title. `<details>` also works. |
| Quote | `> text` |
| Callout | `<callout icon="💡" color="gray_bg">text</callout>` |
| Code | A fenced block with a language. No language or an unknown one saves as `javascript`, so use `txt` for plain text. |
| Divider | `---` |
| Table of contents | `<table-of-contents color="gray" />` |
| Image | `![alt](https://example.com/a.png)` |
| Bookmark, embed | `<bookmark source="..." />`, `<embed source="..." />` |
| Table | A pipe table, or `<table>` |
| Columns | `<columns>` holding `<column ratio="50">` |
| Tabs | `<tabs>` holding `<tab>`, with the first line as the name |
| Mentions | `<mention url="..." />` for a page, `<mention url="user://<person id>" />` for a person, `<date start="2026-11-07" />`. A person the integration can't see becomes the text `@Anonymous`, with no notification. |
| Subpage | `<page url="...">Title</page>` |
| Inline database | `<database url="...">Title</database>` |
| Synced block | `<synced-block url="...">`, `<synced-block-reference url="...">` |
| Anything else | `<unknown url="..." alt="..." />` |

An unclosed tag returns 400 `Markdown errors blocked parsing:` followed by the problems.

## Subpages, databases, and other kept blocks

`<page url>`, `<database url>`, `<unknown>`, synced blocks, and uploaded files stand for blocks already in this document. They only work in the document they came from. You can move the tag to move the block.

| You send | Result |
| - | - |
| Text without a current subpage or inline database | 400 `This operation would delete ...`, unless `allow_deleting_content` is `true`. |
| `<page>Title</page>` with no `url` | Creates a subpage. Up to 100 in one request. |
| `<page url>` of a page that isn't this document's child | A page mention. A URL the integration can't read returns 400. |
| `<database>Title</database>` with no `url` | 400. Databases can't be created. |
| A `<synced-block-reference>` with empty content | Stays as it is. |
| A `<synced-block>` with changed text | Changes the content for every copy. |

## Limits

| Target | Limit |
| - | - |
| Request body | 500 KB |
| Text after edits | 3 MB |
| Text scanned by one request | 20 MB, which is about 40 updates on a 500 KB document |
| Matches in one request | 200,000 |
| New `<page>` tags, new synced blocks | 100 per request |
| Table width | 100 |
| Time to read the Markdown | 5 seconds, then 400 |

The block limits for [Append block children](https://developers.bottari.ai/limits/values.md#page-content) don't apply here, but the document limit does. Long text is split into items of 2,000 characters.

## Response

A `page_markdown` object, the same as [Retrieve as Markdown](https://developers.bottari.ai/api-reference/pages/markdown.md), plus `warnings` when there's something to report.

## Errors

| Status | `code` | Cause |
| - | - | - |
| 400 | `validation_error` | The body or Markdown is wrong, the text wasn't found or matched too many times, the document is in the trash, or a limit was exceeded. |
| 403 | `restricted_resource` | **Update content** is off, a `<page>` tag needs **Insert content**, the database has a task agent attached, the content holds something the integration can't write, or the team reached its block limit. |
| 404 | `object_not_found` | The document doesn't exist, or the integration can't read it. |

See [Common rules](https://developers.bottari.ai/api-reference/overview.md#errors) for shared errors.

```bash curl theme={null}
curl https://api.bottari.ai/v1/pages/exampleRow01/markdown \
  -X PATCH \
  -H "Authorization: Bearer $BOTTARI_TOKEN" \
  -H "Bottari-Version: 2026-03-11" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "update_content",
    "update_content": {
      "content_updates": [
        { "old_str": "Ask the team", "new_str": "Ask the design team" }
      ]
    }
  }'
```

```json 200 theme={null}
{
  "object": "page_markdown",
  "id": "exampleRow01",
  "markdown": "## Before launch\n\nAsk the design team to review it.\n\n- [x] Update the changelog",
  "truncated": false,
  "unknown_block_ids": [],
  "request_id": "33333333-3333-4333-8333-333333333333"
}
```

```json 400 theme={null}
{
  "object": "error",
  "status": 400,
  "code": "validation_error",
  "message": "No matches found for Ask the team.\n\nFetch the page again and base old_str on the current page text.",
  "request_id": "33333333-3333-4333-8333-333333333333"
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.