35 toolsOAuth bearer
notion_example.py
pages_create call does both, and which one you get depends
on the parent you give it.
A database is not the table
Since Notion’s2025-09-03 version a database is a container and a data
source is the table inside it. The columns belong to the data source, rows are
queried from it, and a page created inside a database takes a data_source_id
as its parent.
That matters for the first call you make. databases_retrieve returns the data
sources a database holds; data_sources_retrieve returns the column schema you
have to write against. Querying rows or changing columns through a database ID
is the older shape, which still answers for a database with one table and fails
for one with several.
Authenticating
This pack takes an OAuth bearer token, andconfigure() is optional when $NOTION_API_KEY is set. Which credential provider you hand it depends on whose account the calls run as.
A token you hold
For a script, or a notebook.EnvTokenProvider re-reads the variable on every call, so a token rotated beside the process is picked up without a restart; StaticTokenProvider takes one you already hold as a string. Neither renews anything, so the calls stop when the token expires.
notion_script.py
One account, refreshed
For an agent or a server acting as you.OAuth2Client turns a client registration and a stored refresh token into an access token, and renews it before it lapses.
notion_agent.py
No refresh token yet? Your own account is the one-time consent flow that hands you one.
Many end users
For a product whose users each connect their own account.SubjectProvider builds one credential per user through a factory you write, and use_subject names the user a call acts for. Your users’ accounts is the consent route inside your app; serving many users is the per-subject cache and its eviction.
notion_server.py
ntn_ and goes out as a bearer
token. An OAuth access token works the same way, and so does a personal access
token, with one difference worth knowing: users_list refuses a personal access
token.
Notion’s permissions are not scopes you request per call. An integration is
given capabilities in Notion’s own settings, and each tool page names the one it
needs by the label you tick there. An integration also only sees pages somebody
has shared with it. A page that has not been shared is a 404, not a 403, so an empty
search usually means nothing has been shared rather than that nothing exists.
The client
oauth_tool_factory is the whole client: a thin wrapper over httpx that attaches your token and these endpoint constants to each request. No vendor SDK enters your dependency tree.credentials is whichever of the three you built in Authenticating. A pack takes it through configure(); a client you build takes the same object as credential_provider, and has no configure() of its own.
Paging through a list
A cursor belongs to the tool that returns it, so it is declared on that tool’s builder call:notion_pagination.py
Pagination is declared on
users_list, pages_retrieve_property_item, blocks_children_list, data_sources_templates_list, comments_list, file_uploads_list, custom_emojis_list, search and data_sources_query. The other 26 take no cursor.Notion-Version is required on every request. Omit it and the answer is 400 missing_version, so there is no unpinned mode to fall back to. The pack sends
2026-03-11, which is the version that renamed archived to in_trash and
replaced block append’s after parameter with position.
Tools
Each is aTool, called with
ainvoke as in the snippet above. The name
links to its parameters, its response and what it costs.
Usersusers_listGETList the people and bots in the workspace.users_retrieveGETGet one user by ID.users_retrieve_meGETGet the bot this token authenticates as, including which workspace it is in.SearchsearchPOSTSearch the titles of pages and data sources shared with this integration.Pagespages_createPOSTCreate a page — as a subpage of another page, or as a row of a database by giving its
data_source_id as the parent.pages_retrieveGETGet a page’s properties.pages_updatePATCHUpdate a page’s property values, icon, cover, or trash state.pages_movePOSTMove a page under a different parent page or data source.pages_retrieve_property_itemGETRead one property of a page in full.pages_retrieve_markdownGETGet a page’s whole content as Markdown, rendered by Notion.pages_update_markdownPATCHReplace a page’s content with Markdown, which Notion parses into blocks.Blocksblocks_retrieveGETGet one block.blocks_updatePATCHUpdate a block’s content, or trash and restore it with in_trash.blocks_deleteDELETEMove a block to the trash.blocks_children_listGETList the direct children of a block, or of a page when given a page ID.blocks_children_appendPATCHAppend blocks to a page or a block.Databasesdatabases_createPOSTCreate a database on a page or at the top of the workspace.databases_retrieveGETGet a database and the data sources it contains.databases_updatePATCHUpdate a database’s title, description, icon, cover, inline display or trash state, or move it to a new parent.Data sourcesdata_sources_createPOSTAdd another table to an existing database.data_sources_retrieveGETGet a data source’s column schema — the names, types and select options a write has to match.data_sources_updatePATCHChange a data source’s columns, title or icon.data_sources_queryPOSTGet the rows of a data source, optionally filtered and sorted.data_sources_templates_listGETList the page templates defined on a data source.Commentscomments_createPOSTComment on a page or block, or reply to an existing discussion with its discussion_id.comments_listGETList the unresolved comments on a page or block.comments_retrieveGETGet one comment by ID.comments_updatePATCHEdit a comment’s content, replacing it entirely.comments_deleteDELETEDelete a comment.File uploadsfile_uploads_createPOSTStart a file upload.file_uploads_completePOSTFinish a multi-part upload once every part has been sent.file_uploads_retrieveGETGet a file upload and its status.file_uploads_listGETList this integration’s file uploads, optionally by status.Custom emojiscustom_emojis_listGETList the workspace’s own uploaded emoji.Async tasksasync_tasks_retrieveGETCheck work Notion took in the background.Reading a page
There are three ways, and they cost very different amounts.pages_retrieve returns the properties and none of the content. For a row of a
database that is the whole story, and it is the cheap call.
pages_retrieve_markdown returns the content as Markdown, rendered by Notion.
One call, no recursion, and the result reads like a document. Reach for it
whenever the goal is to understand a page rather than edit a specific block.
blocks_children_list returns the blocks, one level at a time. A child that
reports has_children needs another call with its own ID. Use it when you
intend to edit, since only blocks have the IDs blocks_update needs.
Writing content
Notion accepts at most 100 blocks and two levels of nesting in one request, and the schema says so rather than leaving it to a 400. The block model is three tiers: a top-level block holds nested blocks, a nested block holds leaf blocks, and a leaf block holds nothing. Acolumn and a column_list only exist at the
top; a table cannot be a leaf, because a table is its rows.
Deeper trees are built by appending to the blocks that come back.
The same limit is why pages_create takes a markdown string as an
alternative. Notion parses it server-side into blocks, at any depth, which is
the shortest path from a document you already have to a page that holds it.
Querying a database
data_sources_query takes a filter built from one condition per column type,
composed with and and or. Both are Python keywords, so the fields are spelled
and_ and or_ in Python and go out under Notion’s own names:
data_sources_retrieve first when you do not already know the column names.
What the response handlers drop
Notion has no plain strings. Every piece of text is an array of runs, and each run carries its annotations, aplain_text copy, an href and a type tag, so a
one-line title is about 200 bytes of JSON for about 20 characters of title. A row
repeats that per column and adds two user objects, two timestamps, a parent and
two URLs.
Every read tool projects: a run array becomes the string it spells, a property
becomes its value, a select becomes its name. Nothing is filtered, because a
shorter page would corrupt the length a paginated walk depends on, and every
id is kept, because that is what the next call needs.
data_sources_retrieve is the exception that proves it. There the schema is the
payload, so the columns are kept in full and only the per-option IDs and colours
go.
Known limits
Sending file bytes is not a tool. A file becomes Notion-hosted content in three steps, and the middle one takesmultipart/form-data with raw bytes,
which Charter does not encode and which a model has no way to supply. The other
four upload endpoints are here, and mode="external_url" completes without that
step: Notion fetches the file from a public URL itself.
Coverage is the content API. Users, pages, blocks, databases, data sources,
search, comments, file uploads, custom emojis and async tasks. Notion’s agent
and session platform, the Views API and meeting notes are not modelled, and
neither are the OAuth token endpoints, which belong to an authorization server
rather than to a tool.
pages_create carries a large schema. Every block type is reachable from
it, which is 87KB of JSON schema. That is the cost of being able to write any
block rather than the popular six. Narrow it at the point of use with
Tool.derived when an agent only needs a few.