Skip to Content
SimulatorsNotion changelog

Simulators

Notion changelog

What changed in each Notion simulator release, newest first.

v0.0.11 — 2026-11-06

  1. The docs page has an Errors and limits section. It names the fields of the error object, says the simulator never returns 429, shows how lists page with start_cursor and next_cursor and what each route does with a page_size above 100, and says the simulator reads no idempotency key.

  2. The File uploads page reads more plainly. Sentences are shorter and the places you can attach an upload are a list. Each behavior that differs from Notion is marked “Differs from Notion” where it’s described: where a file is served from, how an external URL upload gets its file and what the free tier refuses.

v0.0.10 — 2026-11-06

  1. The docs name each Notion client’s base URL option. The connect section names baseUrl for the JavaScript client and base_url for the Python one, and links the live feed to check requests arrive.

v0.0.9 — 2026-11-05

  1. Setting the clock never stamps records before the timeline events it fired. When POST /_rystic/clock {"time": ...} fires timeline events due at its target, the next request’s records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before.

  2. A paced clock keeps wall time, and rystic run --speed 0 stops it. Under RYSTIC_PACE, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late. rystic run hands the twin only the pace it resolved, so --speed 0, a scenario without events, --tape and --live keep the clock manual even when your shell exports RYSTIC_PACE.

  3. Indexed list query parameters. filter_properties sent as filter_properties[0]=…&filter_properties[1]=… reads the same as the repeated filter_properties=….

  4. Swagger UI to read and call the twin from the browser. /_rystic/swagger, linked from the state UI header, is Swagger UI over the published API trimmed to the operations this twin serves; Try it out sends each request to the twin.

  5. Any Notion-Version is served. A request naming any Notion-Version, the official SDKs’ default 2025-09-03 included, is served as 2026-03-11 instead of answering 400, so a client works against the twin without pinning the version. A request with no Notion-Version still answers 400 missing_version.

  6. Notion-Version: 2025-09-03 answers as Notion answers it. The official SDKs’ and notion-mcp-server’s default version is served with its own shapes instead of as 2026-03-11: pages, blocks, databases, data sources and file uploads also carry archived, an update reads archived as in_trash and an append reads after as a position, so a client works against the twin on its default settings. At 2026-03-11, archived on a page or block update answers 400 validation_error, as on Notion.

  7. The docs page lists the clients known to work. A new Clients known to work section names each official client checked against this simulator, the release checked, the ways it connects that work and fail, the day it was checked, and any known gap that covers a failure.

  8. @notionhq/client’s first call is held to Notion. The fidelity suite now measures the request the official JavaScript SDK sends first, GET /v1/users?page_size=1 on its default Notion-Version: 2025-09-03, against a live workspace on every round, so a change that breaks the SDK’s start fails the suite.

  9. A slow client no longer holds up the others. The simulator reads a request’s whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients’ requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.

  10. The call log shows what each call asked for and whether it succeeded. Each GET /_rystic/calls entry now carries args, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), and ok, false for an answer that is not 2xx. A path the simulator does not serve is marked unknown_route: true. ?tail=N returns only the last N calls, and dropped counts the calls the log no longer keeps.

  11. The docs are one page, with a page per feature. The archive ships README.md in place of HOWTO.md, with the same sections in the same order as every Rystic simulator’s page, and CHANGELOG.md beside it. Comments, data sources and templates, file uploads, page markdown and the Notion MCP server each have their own page under docs/features/, and every example on these pages runs as written against a fresh simulator.

  12. The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.

  13. The Scenarios table is easier to scan. Each row says in a line what the scenario is for, and a list below the table says what each scenario holds.

  14. Pro starts with a 14-day trial. Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.

  15. Known clients name their registry. The table at the end of Connect your client names each client’s package registry beside it, such as npm, PyPI or GitHub Releases.

  16. Limitations is shorter to read. It opens with the known issues and one sentence counting the rest. The detailed lists follow, and the website folds them behind “Show all limitations”.

  17. The page has no Plans section. The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.

  18. The docs read more plainly and keep to the simulator’s own facts. Sentences are shorter. On the main page and the Data sources page, each behavior that differs from Notion is marked “Differs from Notion” where it’s described. Rules with several outcomes, such as what each Notion-Version accepts, what a deleted comment still returns and what a page template allows, are tables. Seeding state lists every register a seed takes, and the shared pages cover how seeding and reset work in general.

v0.0.8 — 2026-10-12

  1. A world can carry its own timeline. A --seed file, or a complete world document posted to POST /_rystic/seed, may add credentials, binding presented credentials to actors, and events: calls the twin makes on its own API at set sim times as one of those actors, through the same handlers and egress as your requests. A top-level credentials or events key the twin used to ignore now takes effect. RYSTIC_PACE=<speed> plays the clock, and the timeline, in real time, and an event’s at may be +<duration>. /_rystic/inject, --inject and RYSTIC_INJECT now need state_set instead of scenarios.

  2. A timeline call can hand what it minted to later events. A call event may add bind, {"<name>": "<JSON Pointer>"}, saving fields of its answer, such as an ID the twin minted; a later event uses one as ${name} in a call’s path, headers or body or in a seed event’s record keys and values (a register name cannot be bound). A schedule naming a ${name} no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing.

  3. notion_task_tracker plays a timeline. Over the three hours after the pinned clock, Rae ticks a to-do and moves a row to In review, Theo comments and files a new row, and Nour marks the retention policy Done, each through the API under a token the world binds to them; advance the clock to watch the sprint move.

  4. A clock jump fires each timeline event at its own time. When one POST /_rystic/clock or paced tick passes several events, each fires with the clock at its at, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time.

  5. A clock jump stamps each timeline event at its exact instant. When a clock move passes timeline events, the records each one makes carry exactly its at, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn.

  6. A timeline call can carry raw bytes, XML or a JSON array. A call event may send body_text or body_base64 instead of body; the twin sends it as written, under the route’s own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object’s bytes or post an XML document, and ${name} fills in body_text. A body may be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as /InitiateMultipartUploadResult/UploadId.

v0.0.7 — 2026-10-11

  1. The scripts in this archive are MIT-licensed. check.sh and the archive’s other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts , so you can copy, change and share them. LICENSE.txt no longer covers them; it still governs the twin itself.

  2. A file upload’s filename may be up to 900 characters. POST /v1/file_uploads counts a filename’s length in characters, as Notion measures it, so an accented name of 900 characters or fewer is accepted even when it runs past 900 bytes.

  3. Requests Notion refuses are refused, in Notion’s order. The twin answers 400 validation_error where Notion does, before looking anything up: a malformed ID in the path of GET /v1/blocks/{id}/children, GET /v1/data_sources/{id}/templates and PATCH /v1/pages/{id}/markdown; a non-numeric page_size; an unknown filter member, mixed timestamp and property filter, unknown operator or mistyped operand in a data source query; a malformed parent ID, or a template_id template with no ID, on POST /v1/pages; an executable file type, or a filename over the limit, on POST /v1/file_uploads; and a move to a destination the integration can’t reach (tool_error_code: destination_unavailable). GET /v1/data_sources/{id} accepts a dashless ID, and its 404, like a database create’s, carries additional_data.integration_id and request_id.

v0.0.6 — 2026-10-11

  1. A seeded app.notion.com page URL passes the seed gate. app.notion.com is declared Notion’s web host (pipeline_policy.json host_roles): the twin hands such a URL out as written, never rebased, and a seed carrying one lands under RYSTIC_SEED_GATE=reject where it was refused as an undeclared host of the service. Page, database and data-source url values the twin builds still sit on www.notion.so, which Notion no longer uses.

v0.0.5 — 2026-10-10

  1. A sent file upload drops its upload URL, and completing a single-part upload is refused. After a single_part send leaves an upload uploaded, the send’s answer and every read of the upload leave out upload_url and complete_url, as Notion’s do. Completing a single_part upload answers 400 validation_error, “One or more parts must be uploaded before completing the file upload”, where the twin answered 200.

  2. Three more worlds for the Tier 2 surface. notion_page_tree boots a page tree with page and data source parents to move pages between. notion_template_board boots a Tickets data source with five templates, one of them the default, beside a data source with none. notion_attachments boots a page with image and file blocks and an asset library whose files property holds files, ready for an upload to attach.

  3. A moved page leaves its old parent and joins the new one. After POST /v1/pages/{page_id}/move, the old parent’s GET /v1/blocks/{id}/children no longer lists the page, and its has_children updates. The new parent page lists the page as its child_page block. A page moved into a data source drops out of its page parent’s children and appears in the data source’s query.

  4. Templates refuse what Notion refuses and apply their content. POST /v1/pages with a template beside children, content or markdown, or with {"type": "default"} under a data source that has no default template, now answers 400 validation_error with Notion’s message. A page created from a template, or given a template by PATCH /v1/pages/{page_id}, reads back the template’s content as its children. A PATCH adds that content after the page’s existing children.

  5. Attached file uploads read back as files the twin serves. An upload attached as a file, image, PDF, audio or video block, a page icon or cover, or a files property now reads back as {"type": "file", "file": {"url", "expiry_time"}}. The URL serves the uploaded bytes from the twin until expiry_time, one hour after each read. Attaching an upload clears its own expiry_time. GET /v1/file_uploads lists only the calling connection’s uploads, newest first. An upload never attached reads expired once its hour passes on the twin’s clock.

  6. External imports and async tasks complete. An external_url file upload answers pending, as Notion’s does, and reads uploaded with a successful file_import_result on its first GET /v1/file_uploads/{id}. Its file is the twin’s placeholder; the twin never fetches the URL. A POST /v1/pages or PATCH /v1/pages/{page_id}/markdown sent with allow_async: true answers a queued task with poll_after_seconds: 2. Its first GET /v1/async_tasks/{task_id} reads succeeded with the result, where it used to answer 404.

  7. An import’s time reads as Notion writes it. A completed external_url file upload’s file_import_result.imported_time carries a +00:00 offset, as Notion’s does, where it used to end in Z. A client that compares or parses the stamp as text now sees Notion’s form.

  8. A seeded comment reads back without attachments. A comment a shipped world seeds, such as notion_review_thread’s, now answers GET /v1/comments and GET /v1/comments/{comment_id} with no attachments key, as Notion’s comments and the ones POST /v1/comments creates do. It used to carry attachments: [].

  9. A seeded comment’s user mention carries the user. The mention in notion_review_thread’s reply now reads back with the mentioned user’s name, avatar_url and type, as Notion answers a user mention in a comment, beside plain_text @Review Bot. It used to carry only the user’s object and id.

  10. A shipped world’s pages carry Notion’s url. Every page a scenario seeds now reads back with a url on https://app.notion.com/p/, its title’s words and its bare ID, as Notion writes it, and a seeded page mention’s href is the page’s bare https://app.notion.com/p/ url. They used to point at www.notion.so.

  11. A user mentioned in a comment reads back as the user. A rich_text user mention written through POST /v1/comments or PATCH /v1/comments/{comment_id} answers and reads back with the mentioned user’s object, name, avatar_url and type (bot: {} or person), and plain_text @<name>, as Notion does. It used to echo the request’s {id} with empty plain_text.

  12. A comment’s markdown turns a mention-user tag into a mention. <mention-user url="<user id>">…</mention-user> in a comment’s markdown, its url a bare ID or user://<id>, becomes a user mention of that user, the tag’s text dropped: a person resolves to the user and @<name>, a bot to {object, id} and @Anonymous, as Notion answers them. It used to stay literal text.

  13. Every comment 404 names the integration. POST /v1/comments with an unknown discussion_id, parent.page_id or parent.block_id, and GET /v1/comments with an unknown block_id, answer 404 object_not_found with additional_data.integration_id, the calling bot’s ID, as Notion does; a comment on an unknown block is refused, where it used to be created.

  14. Page markdown refuses a database or block ID as Notion does. PATCH /v1/pages/{id}/markdown through a database’s ID answers 400 validation_error “Provided ID <id> is a database, not a page. Use the retrieve database API instead.”, and through a block that is not a page, 400 naming the page that contains it, both with additional_data.tool_error_code invalid_input. GET through a database’s ID answers the same 400, while a block ID still renders its subtree. They used to answer 404, or render the database.

  15. A seeded child database renders like a created one in page markdown. GET /v1/pages/{page_id}/markdown renders every child database with inline and data-source-url read off the database, so notion_team_wiki’s Meeting Notes reads inline="true" data-source-url="collection://…" as a database created by POST /v1/databases does.

  16. Blocks carry every content field Notion returns. A block written as markdown or as children reads back with a heading’s is_toggleable, a paragraph’s icon, a code block’s caption (and no color), a markdown numbered item’s list_format and a markdown file block’s name, as Notion’s do.

  17. A created page’s url is on app.notion.com. A page POST /v1/pages creates, or a markdown <page> tag adds, has its url on https://app.notion.com/p/, its title’s words then its bare ID, as Notion’s are. It used to point at www.notion.so.

  18. Moving a page under itself is refused. POST /v1/pages/{page_id}/move with the page itself as parent.page_id answers 400 validation_error, “Parent ID must be different from the child ID: <id>”, with additional_data.tool_error_code source_is_destination, as Notion does. It used to answer 200.

  19. The template board’s templates hold content. In notion_template_board, each of the Tickets data source’s five templates holds a heading, a paragraph and a to-do or bulleted item of its own, from Bug Report’s steps to reproduce to Customer Call Notes’ attendees. A page created from the default template or from a template_id now reads back that template’s blocks as its children, where it used to read back none.

  20. Eight more routes: page moves, data source templates, async tasks and file uploads. The twin now serves POST /v1/pages/{page_id}/move, GET /v1/data_sources/{data_source_id}/templates, GET /v1/async_tasks/{task_id} and the five file upload routes, 35 operations in all, so the official JavaScript SDK’s pages.move, dataSources.listTemplates, asyncTasks.retrieve and fileUploads methods run against it.

  21. A multi_part upload’s parts and /complete are unmeasured. The measuring workspace is on Notion’s Free plan, which caps a file at 5 MiB and offers no multi-part upload, so creating a multi_part upload is measured but sending its parts and POST /v1/file_uploads/{file_upload_id}/complete are built from Notion’s published spec alone.

v0.0.4 — 2026-10-10

  1. User and property-item reads carry request_id. GET /v1/users/{user_id} and the single-value answer of GET /v1/pages/{page_id}/properties/{property_id} now include Notion’s top-level request_id, as every other answer does.

  2. Every property a data source is created or updated with gets its own ID. POST /v1/data_sources, POST /v1/databases and PATCH /v1/data_sources/{data_source_id} mint a distinct 4-character ID for each new property other than the title (whose ID stays title), and for the synced property a dual_property relation adds to its target, so GET /v1/pages/{page_id}/properties/{property_id}, a properties update, filter_properties and a query’s filter or sort by ID reach the property the ID names. Before, POST /v1/data_sources gave every property it created the same ID, and the other two minted uuids. The twin’s IDs are letters only; Notion’s may also hold URL-encoded punctuation.

v0.0.3 — 2026-10-09

  1. A markdown update_content keeps every block’s ID. PATCH /v1/pages/{page_id}/markdown with update_content edits the page’s blocks in place: every block keeps its ID at any depth, the edited block’s included, so an ID read before the edit still names the same block after it. Before, every block of the page got a new ID.

  2. A markdown replace_content_range keeps every block’s ID. PATCH /v1/pages/{page_id}/markdown with replace_content_range rewrites the matched range in place: every block keeps its ID at any depth, the replaced block’s included, so an ID read before the edit still names the same block after it. Before, every block but a child page or database got a new ID.

  3. A markdown edit that would delete a child page or database is refused in Notion’s words. PATCH /v1/pages/{page_id}/markdown without allow_deleting_content answers 400 validation_error with Notion’s message, which lists each child page or database the edit would delete by title and ID and says how to proceed, and with Notion’s additional_data (tool_error_code invalid_input). Before, the refusal carried a generic message and no additional_data.

  4. A markdown <page> tag with no url creates a subpage. <page>Title</page> in insert_content, in an update_content, replace_content_range or replace_content edit, or in POST /v1/pages markdown creates a child page with that title where the tag stands: the page lists its child_page block, and GET /v1/pages/{id} answers it. A child page whose <page url> tag an edit moves under another block now answers that block as its parent. Before, the tag was dropped, and a moved child page kept the page as its parent.

  5. A connection updates or deletes only a comment it created. PATCH and DELETE /v1/comments/{comment_id} on a comment another user or another connection created answer 404 object_not_found, as Notion’s do, and leave the comment as it was. Before, the twin rewrote or deleted any comment, a person’s included.

  6. A property item list with more pages links to the next one. GET /v1/pages/{page_id}/properties/{property_id} on a title, rich_text, people or relation property with more values than page_size now carries property_item.next_url: the same request with start_cursor set to the answer’s next_cursor, on the twin’s own host. The last page’s next_url stays null. Before, next_url was null on every page.

  7. A two-way relation is kept in sync on both sides. Writing a dual_property relation on a row, through POST /v1/pages or PATCH /v1/pages/{page_id}, updates the synced property on every page the value names or no longer names, so the related pages, GET /v1/pages/{page_id}/properties/{property_id} and a data source query’s relation filter all agree with the write. Creating such a relation, through POST /v1/data_sources or PATCH /v1/data_sources/{data_source_id} with dual_property: {}, adds the matching relation property to the target data source and answers its synced_property_id and synced_property_name, as Notion does.

  8. A markdown edit with allow_deleting_content moves each child page or database it drops to the trash. After PATCH /v1/pages/{page_id}/markdown with allow_deleting_content: true drops a child page’s <page url> tag or a child database’s <database url> tag, through replace_content or update_content, GET /v1/pages/{id} and GET /v1/databases/{id} answer the child with in_trash: true under its parent, and POST /v1/search no longer lists it as live. Before, the child stayed live and its block was gone.

v0.0.2 — 2026-10-09

  1. An empty workspace answers as the twin’s own bot. On Pro and Enterprise, a workspace booted without --scenario or cleared by POST /_rystic/reset still has a bot user: GET /v1/users/me answers it where it answered 401, and the pages and blocks you create name it in created_by. A world that seeds its own bot under state_user.member, as every shipped world does, replaces it.

  2. Moving a data source stamps the database it leaves. A PATCH /v1/data_sources/{data_source_id} that moves the data source to another database now refreshes the previous database’s last_edited_time to the time of the move, as Notion does. The database it joins keeps its last_edited_time.

  3. A page created under a database is a row of it. POST /v1/pages with a database_id parent now lands the page in that database’s data source: its parent reads {type: "data_source_id", data_source_id, database_id} as Notion answers it, and a query of the data source lists it. A page created under a trashed page is refused with 400 validation_error, as Notion refuses it.

  4. A renamed page shows its new title on its block. After PATCH /v1/pages/{id} changes a page’s title, GET /v1/blocks/{id} and the parent’s GET /v1/blocks/{parent}/children answer child_page.title as the new title, as Notion does.

  5. Rich text over 2000 characters is refused, as in Notion. A rich text item whose text.content is longer than 2000 characters is refused with 400 validation_error when you append children (PATCH /v1/blocks/{block_id}/children), update a block or a page title, or set a database or data source title, as POST /v1/pages already refused it in a page title. Measured against a live Notion workspace.

  6. A page trashed through PATCH /v1/pages is trashed as a block too. After PATCH /v1/pages/{page_id} with "in_trash": true, GET /v1/blocks/{page_id} reads in_trash: true, as on Notion, where it read false before.

  7. A data source query sorts by a property and refuses an unknown one, as in Notion. POST /v1/data_sources/{data_source_id}/query now orders rows by a sorts entry naming a property (a number numerically, a title or rich_text by its text), in array order with timestamp sorts. A filter or sort naming a property the data source does not have answers 400 validation_error “Could not find property with name or id: …”, and a condition of another type than its property answers 400 validation_error, as Notion answers both. Before, a property sort was ignored and both answered 200.

  8. A row’s properties follow its data source’s schema. A page under a data source, created with a data_source_id or a database_id parent, now carries every property of the schema, an unset one with its type’s empty value (date: null, checkbox: false, number: null, select: null, title: []). A property can be named by its ID, so title renames the Name title property. A select value reads {id, name, color} from its schema option, and naming an option the schema lacks adds it to the data source with color gray. A property name the schema does not have is refused with 400 validation_error “<name> is not a property that exists.”, as Notion refuses it, with the ID spelled with or without dashes. filter_properties on retrieving or updating a page and on a data source query returns only the properties it names.

  9. A child block of an unknown type is refused, as in Notion. Appending children (PATCH /v1/blocks/{block_id}/children) or creating a page with children refuses a block whose type is not one Notion creates through the API, such as "nonsense", with 400 validation_error, and stores nothing. Before, the twin answered 200 and stored the block.

  10. Trashing a page leaves is_archived alone, as in Notion. PATCH /v1/pages/{page_id} with {"in_trash": true} now answers and reads back in_trash: true with is_archived: false; in_trash and is_archived are independent members. Before, trashing a page also set is_archived: true.

  11. A page created with an over-long rich text block is refused, as in Notion. POST /v1/pages refuses a children or content block whose rich text item is over 2000 characters with 400 validation_error “body failed validation: body.children[0].paragraph.rich_text[0].text.content.length should be ≤ 2000, instead was 2001.”, and stores nothing. Before, the page was created.

  12. A status property starts with Notion’s defaults, and a row shows its unset status and relation. A status property created without options gets Notion’s three options (Not started, In progress, Done) and groups (To-do, In progress, Complete); a row that doesn’t set it reads Not started. An unset relation reads {"relation": [], "has_more": false}, and a relation property’s schema carries database_id beside data_source_id. Before, the status schema was empty and both were left out of the row.

  13. Comments, users, page markdown, property items and data source creation. The twin serves 11 more Notion routes, 27 in all. Agents can post, list, read, edit and delete comments; list the workspace’s users and read one; read a page as Notion’s enhanced markdown and replace or edit it; page through one property’s values; and create a data source in an existing database. Every route at Notion-Version: 2026-03-11 was measured against a live Notion workspace. @notionhq/notion-mcp-server now reaches 22 of its 24 tools.

  14. Comment deletes are soft, and the user list is the workspace’s roster. Deleting a comment keeps it readable by ID with original_content_deleted: true and empty rich_text, drops it from its page’s comment list, and refuses a further edit or delete with 404 object_not_found, as Notion does; a comment’s timestamps are at minute precision. GET /v1/users lists persons before bots, each user once.

  15. Page markdown renders each block’s text. GET /v1/pages/{page_id}/markdown and the answer to PATCH /v1/pages/{page_id}/markdown render each block’s text, a to-do’s checked state and a code block’s language. Before, every block rendered empty ("# \n<empty-block/>"). As a result, update_content and replace_content_range matched nothing, and they wiped the text of every block outside the edit. A block written through markdown also reads back from the blocks API with plain_text and href on its rich text and color on its payload, as Notion’s blocks do.

  16. A block’s comments list by its ID. GET /v1/comments?block_id=… takes a block ID as well as a page ID, as Notion does: a comment created with parent.block_id is listed under that block, apart from its page’s comments, instead of answering 404 object_not_found.

  17. A page created with markdown holds the blocks the markdown describes. POST /v1/pages with a markdown body now stores the blocks Notion’s enhanced markdown describes, converted by the same companion that reads and updates page markdown: a leading # heading is lifted off (it is the title when properties.title is omitted, and is never a block), every other element becomes a block in document order, and a tab-indented line nests under the block above it. GET /v1/blocks/{page_id}/children lists those blocks and GET /v1/pages/{page_id}/markdown renders them back, where before both answered empty. Two readback flows measure this against the live workspace.

  18. Three more worlds for the Tier 1 surface. notion_review_thread boots a task page with two open comment discussions to read, reply in, edit and delete. notion_team_directory boots six people and two bots to page through with GET /v1/users. notion_linked_sources boots one database with two data sources, Projects and Tasks, and a project whose 30-task relation pages through property items. Every shipped world’s own bot now also answers users/me with its workspace ID and workspace limits.

  19. A page answer holds at most 25 relation references and says when there are more. GET /v1/pages/{page_id}, and the page POST /v1/pages and PATCH /v1/pages/{page_id} answer, now carry a relation property’s first 25 references with has_more: true when more exist, as Notion does, so a caller learns it must page through GET /v1/pages/{page_id}/properties/{property_id}, which still returns every reference. The same answer holds at most 25 people in a people value and at most 25 inline mentions in a title or rich_text value. Before, the answer carried every reference with has_more: false.

  20. A markdown edit keeps the child pages and databases it leaves in place. PATCH /v1/pages/{page_id}/markdown with update_content, replace_content_range or replace_content keeps every child page and child database whose <page url> or <database url> tag the edited markdown still carries, under its ID and with its own content, and answers 200 without allow_deleting_content. Before, any such edit on a page holding a child page or database answered 400 validation_error, and with allow_deleting_content: true it deleted the untouched children. An edit that removes a child’s tag still answers 400 unless allow_deleting_content is true.

v0.0.1 — 2026-10-07

  1. A local Notion API for your integration and the agents that drive it. The first release of the Notion twin: 16 routes of the Notion API at Notion-Version: 2026-03-11 — pages, blocks, databases with their data sources, search and the connection’s own bot user — served from a model validated against a live Notion workspace. Point @notionhq/client, notion-client or @notionhq/notion-mcp-server at it with any bearer token and that version (HOWTO.md shows all three); no page lands in a real workspace.

  2. Pages and block trees, written and read back. POST /v1/pages creates a page under a page or a data source, with children if you send them; GET and PATCH /v1/pages/{page_id} read and update it; GET, PATCH and DELETE /v1/blocks/{block_id} and GET and PATCH /v1/blocks/{block_id}/children read, edit and append nested blocks. DELETE trashes rather than deletes, as on Notion, and PATCH with "in_trash": false restores. A missing ID answers Notion’s 404 object_not_found.

  3. Databases, data sources and search. POST /v1/databases creates a database with its first data source; GET and PATCH read and update a database or a data source’s schema; POST /v1/data_sources/{data_source_id}/query returns its rows with property and timestamp filters applied and page_size paging; POST /v1/search finds pages and data sources by title. A property sort is ignored in this release.

  4. One API version, enforced on every route. The twin speaks Notion-Version: 2026-03-11: a request with no Notion-Version or any other version answers 400 missing_version, on every route, as Notion does. The official clients default to 2025-09-03, so set the version in the client.

  5. Five shipped workspaces. --scenario notion_team_wiki boots a wiki with a root page, three child pages, a nested block tree and a Meeting Notes database, and is the world Free boots and POST /_rystic/reset returns to. notion_task_tracker holds a Sprint Tasks database with six rows across every status, notion_blank_slate one page and one empty database, notion_trash_bin a trashed page tree beside a live restore log, and notion_locked_vault locked pages beside an unlocked one; SCENARIOS.md says what each is for. On Pro and Enterprise, POST /_rystic/seed lands pages, blocks, databases, data sources and users directly, and POST /_rystic/reset rewinds the ID sequence and the clock, so two identical runs give byte-identical pages and blocks.

  6. Measured against a live Notion workspace. The 2026-10-07 full-suite round matched 79 of 79 probes against a live Notion workspace, with 0 inconclusive. HOWTO.md states the differences this release ships with: unserved routes answer a plain 404, not Notion’s envelope; a data source query ignores property sorts, has no page_size cap and misses a page created with a database_id parent; and a page’s properties are not resolved against its data source’s schema.

Last updated on