@notionhq/client (5.27.1).
At a glance
Start
--service. When it is the only enabled service, it listens on http://localhost:4000. Without a seed config the workspace (Baseline workspace) has one member (Admin, [email protected]), no pages, and the built-in Baseline integration, which any token acts as (see Auth).
URL mapping
Point the official client at the emulator with
baseUrl:
Auth
SendAuthorization: Bearer <token>.
- Internal integration tokens come from
integrationsin the seed. Each acts as its integration’s bot user with its capabilities (read_content,update_content,insert_content,read_comments,insert_comments, and a user information level) and sees only what is shared with it: thesharedpages and databases and everything under them, plus what it creates. A missing capability gets HTTP 403restricted_resource; anything not shared gets HTTP 404object_not_foundwith Notion’sMake sure the relevant pages and databases are shared with your integration. - OAuth access tokens come from the OAuth flow of a public integration. Each acts as that integration’s bot user for the workspace, with its capabilities and every page.
- Any other token the CLI accepts acts as the built-in
Baselineintegration, with every capability and the whole workspace, so a quick start needs no setup.
unauthorized (API token is invalid.).
Errors
Errors use Notion’s body:{ "object": "error", "status", "code", "message", "request_id" }. Every response carries a request_id. Body checks return HTTP 400 validation_error with Notion’s wording, such as body failed validation: body.children[0].code.language should be a supported language, instead was `"klingon"`., Budget is expected to be number., or Nope is not a property that exists.. Unknown /v1 paths get HTTP 400 invalid_request_url.
Notion’s rate limit is three requests a second on average, answered with HTTP 429 rate_limited and Retry-After. The emulator never limits on its own; to test a client’s handling, inject Notion’s response with fault injection:
@notionhq/client retries 429 responses on its own, honoring Retry-After; pass retry: false to see them.
Versions
Every request needs aNotion-Version header, as on Notion (HTTP 400 missing_version without one). The emulator serves:
2025-09-03, the version@notionhq/client5 sends by default. Databases are containers of data sources; the schema, rows, and queries live on/v1/data_sources, and a database lists its data sources.2022-06-28and the versions before it, answered the 2022-06-28 way: databases carryproperties,POST /v1/databasestakesproperties,PATCH /v1/databases/{id}changes the schema of the database’s data source, and rows have adatabase_idparent.
validation_error.
Pages
POST /v1/pages— Create page: under a page, a data source (data_source_id), a database with one data source (database_id), or a block, withproperties,icon,cover,position, and content aschildren(orcontent) blocks or amarkdownstring (headings, lists, to-dos, quotes, code fences, dividers, and inline bold, italic, code, strikethrough, and links).templatetakesnoneordefault; data sources have no templates.GET /v1/pages/{id}— Get page: withfilter_properties. Relation, people, rich text, and title values show at most 25 entries, withhas_moreon relations.PATCH /v1/pages/{id}— Update page:properties,icon,cover,is_locked,in_trash(orarchived), anderase_content. Pages in the trash, or under a page in the trash, cannot be edited until restored.POST /v1/pages/{id}/move— Move page: to another page or data source.GET /v1/pages/{id}/properties/{property_id}— Get page property: a pagedproperty_itemlist for title, rich text, people, relation, and list rollups (page_sizeup to 100,start_cursor), a single property item otherwise.
Databases and data sources
POST /v1/databases— Create database: withinitial_data_source.properties(orpropertiesunder 2022-06-28),title,description,is_inline,icon, andcover. Exactly one title property is required.GET/PATCH /v1/databases/{id}: title, description, inline, icon, cover, lock, trash, and parent.POST /v1/data_sources— Create data source: adds a data source to a database.GET/PATCH /v1/data_sources/{id}read and change the title, icon, trash state, and schema: add a property, rename it withname, change its type, or remove it withnull.GET /v1/data_sources/{id}/templates— List data source templates: returns an empty list.
Queries
POST /v1/data_sources/{id}/query— Query data source (andPOST /v1/databases/{id}/queryunder 2022-06-28, which reads the database’s first data source) takesfilter,sorts,start_cursor,page_size(up to 100),in_trash(also include rows in the trash), andfilter_propertiesin the query string. Results are apage_or_data_sourcelist (page_or_databaseunder 2022-06-28); without sorts the newest rows come first.
- Property filters name a property by name or id and use its type as the key, such as
{ "property": "Points", "number": { "greater_than": 4 } }. A type that does not match the property getsdatabase property number does not match filter checkbox; an unknown property getsCould not find property with name or id: X. - Conditions for every filterable type: text (
title,rich_text,url,email,phone_number:equals,does_not_equal,contains,does_not_contain,starts_with,ends_with; matching other thanequalsignores case),numberandunique_id(equals,does_not_equal,greater_than,less_than,greater_than_or_equal_to,less_than_or_equal_to),checkbox,selectandstatus(equalsanddoes_not_equal, with a name or a list of names),multi_select(contains,does_not_contain),date,created_time, andlast_edited_time(equals,before,after,on_or_before,on_or_afterwith an ISO date, date and time, ortoday,tomorrow,yesterday,one_week_ago,one_week_from_now,one_month_ago,one_month_from_now;past_week,past_month,past_year,next_week,next_month,next_year,this_week),people,created_by, andlast_edited_by(contains,does_not_contain, with a user id or"me"),relation,files,formula(string,checkbox,number,date),rollup(any,every,nonewith a subfilter, ornumberanddate), andverification(status,does_not_equal). Every type except checkbox and verification also takesis_empty: trueandis_not_empty: true. - Timestamp filters:
{ "timestamp": "created_time", "created_time": { "past_week": {} } }, and the same forlast_edited_time. - Compound filters:
{ "and": [...] }and{ "or": [...] }, nested at most two levels deep, as on Notion.
{ "property", "direction" } or { "timestamp", "direction" }, applied in order. Select and status values sort by option order, and empty values sort last in either direction.
Search
POST /v1/search— Search: matchesqueryagainst page titles and data source titles (database titles under 2022-06-28), ignoring case, over everything the integration can see.filtertakes{ "property": "object", "value": "page" }or"data_source"("database"under 2022-06-28) andin_trash;sorttakes{ "timestamp": "last_edited_time", "direction" }(the default is newest first). Results page bystart_cursorandpage_size.
Blocks
GET /v1/blocks/{id}— Get blockGET /v1/blocks/{id}/children— List block children:page_sizeup to 100,start_cursor.PATCH /v1/blocks/{id}/children— Append block children: up to 100 blocks, nested two levels deep, at the end,aftera block, or at aposition. Tables need rows as wide astable_width; column lists need two columns or more.PATCH /v1/blocks/{id}— Update block: a block’s content for its type, orin_trash.DELETE /v1/blocks/{id}— Delete block: moves it to the trash. Child pages and child databases can only be trashed this way.
Comments
POST /v1/comments— Create comment: starts a discussion on a page (parent.page_id) or block (parent.block_id), or replies to one (discussion_id), withrich_textormarkdown, up to threeattachments(file_upload_id), and adisplay_name(integration, the default;customwith a name;user, for public integrations acting for a user).GET /v1/comments?block_id=— List comments: on a page or block, oldest first, with cursor paging.GET,PATCH(rich_textormarkdown), andDELETE /v1/comments/{id}. An integration may edit and delete only its own comments.
File uploads
POST /v1/file_uploads— Create file upload: withmodesingle_part(the default),multi_part(filenamewith an extension andnumber_of_partsup to 1000), orexternal_url(an https URL), plusfilenameandcontent_type. The response hasupload_url(andcomplete_urlfor multi part uploads) on the emulator.POST /v1/file_uploads/{id}/send— Send file upload: takes multipart form data withfileand, for multi part uploads,part_number. Single part files and parts are at most 20 MB, and every part but the last is at least 5 MB. A file whose type differs from the upload’scontent_typeis refused.POST /v1/file_uploads/{id}/complete— Complete file upload: finishes a multi part upload once every part is sent.GET /v1/file_uploads/{id}— Get file upload: one of the integration’s own uploads.GET /v1/file_uploads— List file uploads:status, cursor paging; the integration’s own uploads.
{ "type": "file_upload", "file_upload": { "id": "..." } } in an icon, a cover, an image, video, audio, file, or PDF block, a files property value, or a comment attachment. Pages then return a Notion hosted file, { "type": "file", "file": { "url", "expiry_time" } }, served by the emulator at /_notion/files/{id}/{name} until its URL expires an hour later (HTTP 403 after). An upload not attached within an hour of creation expires. External URL imports finish at once, since the emulator runs without network access, and their hosted URL redirects to the original.
OAuth
Public integrations come fromoauth_integrations in the seed, each with a client_id, client_secret, redirect_uris, and capabilities. With none seeded, any client id is accepted: it is registered on first use with every capability, and the first client secret sent to the token endpoint sticks.
GET /v1/oauth/authorize?client_id=...&redirect_uri=...&response_type=code&owner=user&state=...— Authorize: shows a consent page listing the integration’s capabilities and the workspace members. Choosing a member redirects toredirect_uriwithcodeandstate; Cancel redirects witherror=access_denied. Aredirect_urithat matches none of the integration’s gets an error page.POST /v1/oauth/token— Exchange token: takes the client id and secret as HTTP Basic auth and a JSON body:{ "grant_type": "authorization_code", "code", "redirect_uri" }(the sameredirect_urithe authorize step had; codes are single use and last ten minutes) or{ "grant_type": "refresh_token", "refresh_token" }. The answer hasaccess_token,token_type: "bearer",refresh_token,bot_id,workspace_id,workspace_name,workspace_icon,owner(the member who added it), andduplicated_template_id: null. A refresh issues a new pair and ends the old one. Each integration has one bot user per workspace, whoever installs it.POST /v1/oauth/introspect— Introspect token: with{ "token" }answers{ active, scope, iat }, wherescopelists the capabilities.POST /v1/oauth/revoke— Revoke token: with{ "token" }ends it (revoking a refresh token also ends its access token). Revoked tokens get HTTP 401.
{ "error", "error_description", "request_id" }: invalid_client (HTTP 401) for missing or wrong credentials, invalid_grant for a bad, used, or expired code or refresh token or a different redirect_uri, and unsupported_grant_type. The client’s client.oauth.token, introspect, and revoke work unchanged.
Users
GET /v1/users— List users: pagedGET /v1/users/{id}— Get userGET /v1/users/me— Get bot user: returns the integration’s bot with its workspace
with_email user information level; with none, the list and retrieve endpoints answer HTTP 403.
Webhooks
Notion sets webhook subscriptions up in an integration’s settings, so the emulator takes them from the seed or a local control endpoint:webhookson an entry inintegrationsoroauth_integrations:url,events(all when left out),api_version(2025-09-03by default, or2022-06-28), andverification_token. With a token the subscription starts verified; without one the emulator sends the verification request at startup.POST /_notion/webhooks— Create webhook subscription: with{ "integration": <name, id, token, or client id>, "url", "events", "api_version" }creates a subscription and sends{ "verification_token": "secret_..." }to its URL, as Notion does. The answer hassubscription_id,status: "pending", andverification_request_status. Confirm withPOST /_notion/webhooks/{subscription_id}/verifyand{ "verification_token" }; remove withDELETE /_notion/webhooks/{subscription_id}.
page.created, page.content_updated, page.properties_updated, page.moved, page.deleted, page.undeleted, page.locked, page.unlocked, database.* and data_source.* (created, moved, deleted, undeleted, content_updated, schema_updated), and comment.created, comment.updated, comment.deleted. Each delivery is one event:
updated_blocks and updated_properties joined. A 2025-09-03 subscription gets data_source.content_updated and data_source.schema_updated and parents like { "type": "database", "id", "data_source_id" }; a 2022-06-28 subscription gets database.content_updated and database.schema_updated for a database’s first data source instead, and no other data_source.* events.
Every event carries X-Notion-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body keyed by the verification token; check it with the client’s verifyWebhookSignature. Any 2xx answer counts as delivered. Other answers and network errors are retried up to eight times, after 1, 2, 4, 8, 16, and 32 seconds and then twice a minute apart (Notion spreads its retries over about a day).
Seed config
capabilities: all when left outuser_info: none, without_email, or with_email (the default)shared: page or database titles or ids; the whole workspace when left outevents: all when left outapi_version: or “2022-06-28”verification_token: verified at once; left out, the handshake runs at startupicon: an emoji, or an image URLcontentitems: a string is a paragraph,{ "heading_1": "Policies" }is a block type with its text, or give a whole block object (theto_doitem)parent: top level when left out
{ number: <format> }, { select: [...] }, { multi_select: [...] }, { status: [...] }, { formula: <expression> }, { unique_id: <prefix> }, { relation: <database title> } or { relation: { database, dual } }, and { rollup: { relation, property, function } }. Any other value is passed through as Notion’s property configuration. Row values are plain: text, numbers, booleans, option names, dates, user emails, file URLs, and related row titles. Seeds are checked as the API checks writes, so a bad seed stops the emulator with a message naming the entry. Seeding again does not duplicate pages, databases, or rows (matched by title).
Inspector
Open the emulator’s root URL for tabs listing pages, databases with their data sources, schemas, and row counts, comments and file uploads, users and integrations with their tokens, capabilities, and what is shared with them, and OAuth integrations, webhook subscriptions with their verification tokens, and recent deliveries with each attempt. Page and database URLs from the API open a view of the page’s content and properties, or the database’s rows.Limits
- Formulas are not evaluated, so formula filters see an empty string. Data source templates, views, and wiki verification are not modeled.
- Search matches title substrings and has no relevance ranking:
sort: { property: "relevance" }is accepted and sorts by last edit. - Ids are accepted with or without dashes. Times are rounded down to the minute, as Notion reports them.
Conformance
See CONFORMANCE.md for every provider.