The Notion API supports reading, writing, and updating page content using enhanced markdown (also called “Notion-flavored Markdown”) as an alternative to the block-based API. This is especially useful for agentic systems and developer tools that work natively with markdown.Three API surfaces are available:
Operation
Endpoint
Description
Create
POST /v1/pages
Create a page with markdown content (via markdown body param)
<meeting-notes> (transcript included when include_transcript=true)
For file-based blocks (image, file, video, audio, PDF), the URLs in the markdown output are pre-signed and ready to download. They expire after a short period, consistent with the block-based API.
The following block types are not yet rendered in the markdown output. When encountered, they appear as <unknown url="..." alt="block_type"/> tags. The url links to the block in Notion, and alt indicates the original block type.
Use POST /v1/pages with the markdown parameter instead of children to create a page from a markdown string.
The markdown field expects actual newline characters. In JSON, use \n to encode them — for example, "# Heading\n\nParagraph". When using cURL, wrap the --data body in single quotes so that \n is preserved for the JSON parser.
Some blocks in a page may appear as <unknown> tags in the markdown output. This can happen for two reasons:
Truncation — the page exceeds the record limit (approximately 20,000 blocks) and some blocks were not loaded.
Permissions — the page contains child pages or other content that is not shared with the integration. The integration can access the parent page, but not those specific child blocks.
In both cases:
The truncated field is set to true.
The affected blocks appear as <unknown url="..." alt="..."/> tags in the markdown.
The unknown_block_ids array contains the IDs of these blocks.
For blocks that were unknown due to truncation, this returns the subtree rooted at that block. For blocks that are unknown due to permissions, the request returns an object_not_found error — the integration does not have access to that content.
The unknown_block_ids array does not distinguish between truncated and inaccessible blocks. When re-fetching unknown block IDs, handle object_not_found errors gracefully as they indicate blocks the integration cannot access.
For the best experience, keep pages under a few thousand blocks. Very large pages may require multiple requests to fully retrieve.
Use PATCH /v1/pages/:page_id/markdown to insert or replace content in an existing page using markdown.The request body uses a discriminated union with four command variants: insert_content, replace_content_range, update_content, and replace_content.
The content field expects standard markdown with actual newline characters. In your JSON request body, use \n to encode newlines — for example, "## Heading\n\nParagraph text" creates a heading followed by a paragraph. Literal backslash-n sequences (like typing \n into a form field) will not be interpreted as newlines.When using cURL, wrap the --data body in single quotes so that \n is preserved for the JSON parser. Avoid $'...' quoting, which converts \n into a literal newline and produces invalid JSON.
curl -X PATCH 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \ -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \ -H "Content-Type: application/json" \ -H "Notion-Version: 2026-03-11" \ --data '{ "type": "insert_content", "insert_content": { "content": "## Appendix\n\nAdded at the end of the page." } }'
The after parameter uses an ellipsis-based selection format: "start text...end text". This matches a range from the first occurrence of the start text to the end text. When after is omitted, content is appended to the end of the page.
Use update_content to make targeted edits with an array of search-and-replace operations. Each operation specifies old_str (content to find) and new_str (replacement content).
Each old_str must match exactly one location in the page. If it matches multiple locations, a validation_error is returned — set replace_all_matches: true on that operation to replace all occurrences.
By default, the update endpoint refuses to delete child pages or databases. If an operation would delete them, a validation_error is returned listing the affected items.To allow deletion, set allow_deleting_content: true in the command body. This option is supported by replace_content_range, update_content, and replace_content:
The update endpoint always skips meeting note transcript content, matching the default behavior of the GET endpoint. If you retrieve a page with include_transcript=true, the transcript text will appear in the response but cannot be used in content_range or after selections — the update endpoint does not see transcript content during matching and will return a validation_error for selections that span transcript text.