Notion

Lightweight, dependency-free, in-memory fake of the Notion API for testing code that uses the real @notionhq/client SDK or the language-agnostic /v1 REST surface.

Default port: 4794

Notion requires an Authorization: Bearer <token> header and a Notion-Version header — a request to any /v1/* route without Notion-Version returns 400 missing_version, exactly like the real API. Objects carry the documented { object, id, ... } shape; collections carry { object: "list", results, next_cursor, has_more }.

Quick start

import { NotionServer } from "./services/notion/src/server.js";

const server = new NotionServer(4794);
await server.start();
// ... run your app/tests ...
await server.stop();
import { Client } from "@notionhq/client";
const notion = new Client({ auth: "secret_xxx", baseUrl: "http://127.0.0.1:4794" });

const page = await notion.pages.create({
  parent: { database_id: "<dbId>" },
  properties: { Name: { title: [{ text: { content: "Hello" } }] } },
});
// => { object: "page", id, properties, ... }

Access via MCP / preview URL

Point your MCP server / agent tooling at the preview URL printed by the parlel pool (defaults to http://127.0.0.1:4794). Set NOTION_BASE_URL to that URL and supply any non-empty NOTION_API_KEY; the fake accepts any Bearer token and any Notion-Version value — but the Notion-Version header must be present (the official @notionhq/client always sends it).

Implemented operations

All /v1/* routes require an Authorization: Bearer <token> header and a Notion-Version header.

Pages

Databases

Search

Users

Service & inspection

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatus
Page create / get / update (full object shape)✅ Supported
Notion-Version header required (missing_version)✅ Supported
parent.type normalization (database_id / page_id / workspace)✅ Supported
DB-parent property schema validation (title required, type/name checks)✅ Supported
Database get / query✅ Supported
Search (pages + databases, query + type filter)✅ Supported
users/me (bot owner, workspace_id, workspace_limits)✅ Supported
List envelope (object:"list", next_cursor, has_more)✅ Supported
Error envelope (object:"error", invalid_json / validation_error / object_not_found)✅ Supported
Block children (/v1/blocks/:id/children)⟳ Roadmap
Database create⟳ Roadmap — a default DB always exists
Rich filter / sort grammar on query◐ Returns all rows
Search relevance ranking◐ Substring match
Pagination cursors◐ Single page (has_more:false)
Token validity / capability enforcement (403) / rate limit (429)✓ By design — any non-empty credential is accepted — no real secrets needed

Error shapes

Errors use the Notion envelope { object: "error", status, code, message }.

StatuscodeWhen
400invalid_jsonrequest body could not be parsed as JSON ("Error parsing JSON body.")
400missing_versionNotion-Version header absent on a /v1/* request
400validation_errormissing required field / property not in schema / type mismatch
401unauthorizedno Authorization: Bearer header ("API token is invalid.")
404object_not_foundunknown page / database / endpoint
405invalid_requestmethod not allowed

Manifest

See services/notion/manifest.json:

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

NOTION_API_KEY=secret_parlel
NOTION_BASE_URL=http://parlel-bridge:4794
<!-- parlel:testenv:end -->