Simulators
Page markdown
Page markdown lets an agent read a whole page as one markdown string and edit it by search and replace, instead of walking its blocks. The blocks API reads the same edit, because both views are one page:
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"notion_team_wiki"}'
curl -s "$RYSTIC_URL/v1/pages" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -H 'Content-Type: application/json' -d '{"parent":{"page_id":"a1000000-0000-4000-8000-000000000101"},"markdown":"# Release notes\n\nDraft one\n\n- [ ] Tag it"}' | jq -c '{id}'
# -> {"id":"73d01b77-cb6c-4662-8000-000000000001"}
curl -s -X PATCH "$RYSTIC_URL/v1/pages/73d01b77-cb6c-4662-8000-000000000001/markdown" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -H 'Content-Type: application/json' -d '{"type":"update_content","update_content":{"content_updates":[{"old_str":"Draft one","new_str":"Final copy"}]}}' | jq -c '{markdown}'
# -> {"markdown":"Final copy\n- [ ] Tag it"}
curl -s -X PATCH "$RYSTIC_URL/v1/pages/73d01b77-cb6c-4662-8000-000000000001/markdown" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -H 'Content-Type: application/json' -d '{"type":"update_content","update_content":{"content_updates":[{"old_str":"no such text","new_str":"x"}]}}' | jq -c '{status, code}'
# -> {"status":400,"code":"validation_error"}
curl -s "$RYSTIC_URL/v1/blocks/73d01b77-cb6c-4662-8000-000000000001/children" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' | jq -c '.results | map(.type)'
# -> ["paragraph","to_do"]
Reading
GET /v1/pages/{page_id}/markdown renders a page’s blocks as Notion’s enhanced markdown. A block with no markdown form renders as <unknown>, and its id is listed in unknown_block_ids.
Writing
PATCH /v1/pages/{page_id}/markdown takes one command and returns the page’s new markdown:
| Command | What it does |
|---|---|
update_content | searches and replaces |
replace_content | rewrites the whole page |
insert_content or replace_content_range | the older commands, which edit part of the page |
A selection that matches nothing returns 400 validation_error.
POST /v1/pages with markdown creates the blocks the markdown describes. A leading # line is lifted off as the page’s title and never becomes a block.
Async tasks
Creating a page with markdown, or updating a page’s markdown, with allow_async: true returns 202 with a queued task and poll_after_seconds: 2. GET /v1/async_tasks/{task_id} reads it as succeeded, with the result, on the first poll, as Notion’s does:
curl -s "$RYSTIC_URL/v1/pages" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -H 'Content-Type: application/json' -d '{"parent":{"page_id":"a1000000-0000-4000-8000-000000000101"},"markdown":"# Async page","allow_async":true}' | jq -c '{object, status, poll_after_seconds}'
# -> {"object":"async_task","status":"queued","poll_after_seconds":2}
curl -s "$RYSTIC_URL/v1/async_tasks/task_ffab1951720d4c8c8000000000000001" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' | jq -c '{object, status}'
# -> {"object":"async_task","status":"succeeded"}