Skip to main content

Overview

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:
OperationEndpointDescription
CreatePOST /v1/pagesCreate a page with markdown content (via markdown body param)
ReadGET /v1/pages/:page_id/markdownRetrieve a page’s full content as markdown
UpdatePATCH /v1/pages/:page_id/markdownInsert or replace content using markdown
All three endpoints use the same enhanced markdown format. See the Enhanced markdown format reference for the full specification.

Creating a page with markdown

Use POST /v1/pages with the markdown parameter instead of children to create a page from a markdown string.
curl -X POST https://api.notion.com/v1/pages \
  -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
  -H "Content-Type: application/json" \
  -H "Notion-Version: 2025-09-03" \
  --data '{
    "parent": { "page_id": "YOUR_PAGE_ID" },
    "markdown": "# Meeting Notes\n\nDiscussed roadmap priorities.\n\n## Action items\n\n- [ ] Draft proposal\n- [ ] Schedule follow-up"
  }'
Key behaviors:
  • The markdown parameter is mutually exclusive with children and content. You cannot use both.
  • If properties.title is omitted, the first # h1 heading is extracted as the page title.
  • Available to all integration types (public and internal).
  • Requires insert_content and insert_property capabilities.
The response is a standard page object.

Retrieving a page as markdown

Use GET /v1/pages/:page_id/markdown to retrieve a page’s content rendered as enhanced markdown.
curl 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
  -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
  -H "Notion-Version: 2025-09-03"
Response:
{
  "object": "page_markdown",
  "id": "page-uuid",
  "markdown": "# Meeting Notes\n\nDiscussed roadmap priorities.\n\n## Action items\n\n- [ ] Draft proposal\n- [ ] Schedule follow-up",
  "truncated": false,
  "unknown_block_ids": []
}

Query parameters

ParameterTypeDescription
include_transcriptbooleanInclude meeting note transcripts (default: false).
curl 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown?include_transcript=true' \
  -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
  -H "Notion-Version: 2025-09-03"
This endpoint is restricted to public integrations only. Internal integrations (workspace-level bots) will receive a restricted_resource error. Use the block-based API instead for internal integrations.
Key behaviors:
  • Requires read_content capability.
  • File URIs in the content are automatically converted to pre-signed URLs.

Handling large pages (truncation)

Pages are rendered up to a limit of approximately 20,000 records. When a page exceeds this limit:
  1. The truncated field is set to true.
  2. Blocks that could not be loaded appear as <unknown> tags in the markdown.
  3. The unknown_block_ids array contains the IDs of these unloaded blocks.
{
  "object": "page_markdown",
  "id": "page-uuid",
  "markdown": "# Large Document\n\nFirst section content...\n\n<unknown url=\"https://notion.so/abc123#def456\"/>",
  "truncated": true,
  "unknown_block_ids": ["def456-with-dashes-uuid"]
}
You can fetch the content of unloaded blocks by passing their IDs back to the same endpoint:
curl 'https://api.notion.com/v1/pages/UNKNOWN_BLOCK_ID/markdown' \
  -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
  -H "Notion-Version: 2025-09-03"
This returns the subtree rooted at that block, rendered as markdown. The block’s permissions are validated through its ancestors.
For the best experience, keep pages under a few thousand blocks. Very large pages may require multiple requests to fully retrieve.
Example: iteratively fetching a large page
import requests

headers = {
    "Authorization": f"Bearer {NOTION_API_KEY}",
    "Notion-Version": "2025-09-03",
}

resp = requests.get(
    f"https://api.notion.com/v1/pages/{page_id}/markdown",
    headers=headers,
).json()

all_markdown = resp["markdown"]

for block_id in resp.get("unknown_block_ids", []):
    block_resp = requests.get(
        f"https://api.notion.com/v1/pages/{block_id}/markdown",
        headers=headers,
    ).json()
    all_markdown += "\n" + block_resp["markdown"]

Updating a page with markdown

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 two command variants: insert_content and replace_content_range.

Inserting content

Insert new markdown content after a specific point in the page, or append to the end.
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: 2025-09-03" \
  --data '{
    "type": "insert_content",
    "insert_content": {
      "content": "## New Section\n\nInserted content here.",
      "after": "# Meeting Notes...Action items"
    }
  }'
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: 2025-09-03" \
  --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.

Replacing content

Replace a matched range of existing content with new markdown.
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: 2025-09-03" \
  --data '{
    "type": "replace_content_range",
    "replace_content_range": {
      "content": "## Updated Section\n\nNew content replaces the old.",
      "content_range": "## Old Section...end of old content"
    }
  }'
The content_range parameter uses the same ellipsis-based selection as after.

Safety: protecting child pages and databases

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:
{
  "type": "replace_content_range",
  "replace_content_range": {
    "content": "Replacement content.",
    "content_range": "start...end",
    "allow_deleting_content": true
  }
}

Update response

Both variants return the full page content as markdown after the update:
{
  "object": "page_markdown",
  "id": "page-uuid",
  "markdown": "...full page content after update...",
  "truncated": false,
  "unknown_block_ids": []
}
Key behaviors:
  • Available to all integration types (public and internal).
  • Requires update_content capability.
  • The content_range / after matching is case-sensitive.

Error responses

Error codeCondition
validation_errorThe content_range or after selection does not match any content in the page.
validation_errorThe operation would delete child pages or databases and allow_deleting_content is not true. The error message lists the affected items.
validation_errorThe provided ID is a database or non-page block (use the appropriate API for those record types).
validation_errorThe target page is a synced page (external_object_instance_page). Synced pages cannot be updated.
object_not_foundThe page does not exist or the integration does not have access to it.
restricted_resourceThe integration lacks update_content capability.

Meeting note transcripts

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.

Access control summary

EndpointPublic integrationsInternal integrationsRequired capability
Create (POST /v1/pages)YesYesinsert_content
Read (GET .../markdown)YesNoread_content
Update (PATCH .../markdown)YesYesupdate_content