Skip to Content

Simulators

Notion simulator

notion-twin is a local, stateful simulator 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. It never calls Notion, and no test page lands in a real workspace.

Quickstart

You need an API key from Account → API keys  (how).

curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…                                               # your API key
rystic run notion-twin -d --port 0 --scenario notion_team_wiki   # prints the URL it serves on
curl -s http://localhost:<port>/_rystic/version

Expect "product":"notion-twin". --scenario notion_team_wiki boots the same workspace on every plan. The examples below use localhost:8080; use the port rystic run printed.

On Enterprise, rystic login ./rystic-license.json takes the place of the key (license files).

Add a page under the wiki’s root, then write a paragraph into it:

curl -s localhost:8080/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"},"properties":{"title":{"title":[{"text":{"content":"Launch checklist"}}]}}}'
# -> {...,"id":"73d01b77-cb6c-4662-8000-000000000001",...,"object":"page",...}

curl -s -X PATCH localhost:8080/v1/blocks/73d01b77-cb6c-4662-8000-000000000001/children \
  -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -H 'Content-Type: application/json' \
  -d '{"children":[{"object":"block","type":"paragraph","paragraph":{"rich_text":[{"type":"text","text":{"content":"Ship it"}}]}}]}'
# -> {...,"object":"list","results":[{...,"type":"paragraph",...}],...}

The version pin

Point the SDK at the twin, and set the version: the twin speaks 2026-03-11 only, and the official clients default to 2025-09-03.

// @notionhq/client
const notion = new Client({ auth: "secret_placeholder", baseUrl: "http://localhost:8080", notionVersion: "2026-03-11" });
# notion-client
notion = Client(auth="secret_placeholder", base_url="http://localhost:8080", notion_version="2026-03-11")
  1. A request with no Notion-Version header answers 400 missing_version.
  2. 2025-09-03 or any other value answers 400 missing_version on every route, as Notion answers a version it does not serve.

Plans

What notion-twin does on each plan (plans):

FreeProEnterprise
CredentialAPI keyAPI keylicense file
Bootsnotion_team_wikian empty workspace, or the --scenario you namesame as Pro
POST /_rystic/resetre-applies notion_team_wikiwipes to emptywipes to empty
Notion API (all 16 routes)yesyesyes
Control planeversion, routes, reset onlyevery routeevery route

Free runs 5 hours of simulator time a month. It serves the whole Notion surface against notion_team_wiki. Every control route beyond GET /_rystic/version, GET /_rystic/routes and POST /_rystic/reset answers 403, naming the feature it needs (the loopback-only GET /_rystic/activity aside). Free also refuses --scenario naming any other world, --seed and RYSTIC_NETWORK; see boot inputs.

Pro opens every control route. Without --scenario the twin boots an empty workspace, and POST /_rystic/reset wipes it back to empty.

Enterprise is Pro on a license file instead of an API key.

Auth

Notion reads an internal connection’s token as Authorization: Bearer <token>. The twin accepts any non-empty bearer token and answers as the world’s bot user; no Notion workspace or token is involved.

  1. A request with no bearer token answers 401 unauthorized.
  2. An empty workspace has no bot user, so every request answers 401: boot a scenario or seed a state_user bot first.

Coverage

16 routes. GET /_rystic/routes returns the same list from a running twin.

ResourceRoutes
Pages (3)POST /v1/pages; GET, PATCH /v1/pages/{page_id}
Blocks (5)GET, PATCH, DELETE /v1/blocks/{block_id}; GET, PATCH /v1/blocks/{block_id}/children
Databases (3)POST /v1/databases; GET, PATCH /v1/databases/{database_id}
Data sources (3)GET, PATCH /v1/data_sources/{data_source_id}; POST /v1/data_sources/{data_source_id}/query
Search (1)POST /v1/search
Users (1)GET /v1/users/me

Not modelled: comments, file uploads, views, page markdown, page moves, page property items, data source creation and templates, the user and member lists, and the deprecated POST /v1/databases/{database_id}/query. An unserved route answers 404 {"message":"No route in twin for …"}, not Notion’s error envelope.

The Notion MCP server

@notionhq/notion-mcp-server reaches the twin through its BASE_URL environment variable. Pass the version with the token, since the server otherwise sends 2025-09-03 on most tools:

{
  "mcpServers": {
    "notion": {
      "command": "npx",
      "args": ["-y", "@notionhq/notion-mcp-server"],
      "env": {
        "BASE_URL": "http://localhost:8080",
        "OPENAPI_MCP_HEADERS": "{\"Authorization\":\"Bearer secret_placeholder\",\"Notion-Version\":\"2026-03-11\"}"
      }
    }
  }
}

Of the server’s 24 tools, 14 reach a served route. These 10 answer the twin’s 404: API-get-user, API-get-users, API-retrieve-a-page-property, API-retrieve-a-comment, API-create-a-comment, API-create-a-data-source, API-list-data-source-templates, API-move-page, API-retrieve-page-markdown and API-update-page-markdown.

Control plane

Every route under /_rystic/ is listed, with the plan that reaches it, in routes by plan. Notion declares no outbound effects, so GET /_rystic/egress is always empty, and it has no WebSocket feed.

Scenarios

Five shipped worlds, each on 2026-02-01T00:00:00Z; Free boots notion_team_wiki.

ScenarioWhat it holds
notion_team_wikia workspace root page with three child pages, a nested block tree and an inline Meeting Notes database with one row
notion_task_trackera Sprint Tasks database with title, select, date, people and checkbox properties and six rows across every status
notion_blank_slateone bot, one page holding an empty paragraph and an empty database with only a title property
notion_trash_bina Q3 Planning page in the trash with its subpage, blocks and database, beside a live restore log
notion_locked_vaultpages, a database and a row carrying is_locked, with one unlocked scratch page

Seeding state

Pro and Enterprise. POST /_rystic/seed takes a register map — state_page, state_block, state_database, state_data_source, state_user — and lands it directly; GET /_rystic/registers lists every field. --seed <file> or RYSTIC_SEED=<file> boots one.

Determinism & reset

Ids are a sequence (73d01b77-cb6c-4662-8000-000000000001, then …0002) and the clock starts at a pinned instant — 2026-01-01T00:00:00Z empty, 2026-02-01T00:00:00Z in every scenario — and moves one millisecond per request that stamps a time, never by wall time. Two identical runs from the same reset give byte-identical pages and blocks.

Limitations

  1. The version pin above: send 2026-03-11; any other version answers 400.
  2. Unserved routes answer a plain 404, not Notion’s object_not_found envelope.
  3. A data source query sorts by created_time or last_edited_time only (a property sort is ignored), has no page_size cap, and misses a page created with a database_id parent: give rows a data_source_id parent.
  4. A page’s properties are not resolved against its data source’s schema.
  5. No rate limits: never a 429.
  6. Ids and request_id values are the twin’s own sequences, not Notion’s random ids, and page urls point at www.notion.so but serve nothing.

Fidelity is scored differentially against a live Notion workspace through an internal connection, per response field: the last full-suite round matched 79 of 79 probes on 2026-10-07. Each release states its own number.

The full guide ships in the release as HOWTO.md; rystic docs notion-twin prints it.

Release notes

Newest first.

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. (RYS-1947)

  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. (RYS-1597, RYS-1598, RYS-1884)

  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. (RYS-1595, RYS-1596, RYS-1886, RYS-1892)

  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. (RYS-1914)

  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. (RYS-1947)

  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, each with its ticket: unserved routes answer a plain 404, not Notion’s envelope (RYS-1913); a data source query ignores property sorts, has no page_size cap and misses a page created with a database_id parent (RYS-1596); and a page’s properties are not resolved against its data source’s schema (RYS-1885). (RYS-1947)

Last updated on